home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
ADA Programming Guide
/
ADA Programming Guide.iso
/
ada_guid
/
style-t.hlp
< prev
Wrap
Text File
|
1996-01-30
|
553KB
|
16,229 lines
Ada Quality and Style:
Guidelines for Professional Programmers
SPC-91061-CMC
VERSION 02.01.01
December 1992
Ada Quality and Style:
Guidelines for Professional Programmers
SPC-91061-CMC
VERSION 02.01.01
December 1992
SOFTWARE PRODUCTIVITY CONSORTIUM, INC.
SPC Building
2214 Rock Hill Road
Herndon, Virginia 22070
Copyright 1989, 1991,1992 Software Productivity Consortium, Inc., Herndon,
Virginia. Permission to use, copy, modify and distribute this documentation
for any purpose and without fee is hereby granted, provided that the above
copyright notice appears in all copies and that both this copyright notice and
this permission notice appear in supporting documentation, and that the name
Software Productivity Consortium not be used in advertising or publicity
pertaining to distribution of the guidelines without the specific, written
prior permission of the Consortium. Software Productivity Consortium, Inc.
makes no representations about the suitability of the guidelines described
herein for any purpose and they are provided "as is" without express or
implied warranty.
-----
Ada-ASSURED is a trademark of GrammaTech, Inc.
IBM is a registered trademark of International Business Machines Corporation.
VAX is a registered trademark of Digital Equipment Corporation.
PREFACE
This version of Ada Quality and Style: Guidelines for Professional Programmers
was updated under contract to the Department of Defense (DoD) Ada Joint
Program Office (AJPO). Considerable effort was placed on improving the
coverage of portability and reusability issues, so the majority of changes can
be found in those chapters. A new chapter has been included to address
performance issues.
The most visible change, however, involves the capitalization issue. The
guideline has not changed, but the instantiation now recommends mixed-case
identifiers with upper-case abbreviations and lower-case reserved words.
Several informal surveys and the general consensus of the reviewers showed
strong support for this change.
The Complete Examples chapter includes two new examples which highlight
portability and the use of tasking. Each of the examples in this chapter are
intended to be compilable and executable.
The Consortium invites comments on this guidebook to continue enhancing its
quality and usefulness. We will consider suggestions for current guidelines
and areas for future expansion. Examples that highlight particular points are
most helpful.
Please direct comments to:
Technology Transfer Division - AQS
Software Productivity Consortium
SPC Building
2214 Rock Hill Road
Herndon, Virginia 22070#4005
(703) 742-7211
DoD activities and Defense contractors can obtain copies of this document
through the Defense Technical Information Center:
DTIC
Cameron Station
Alexandria, VA 22314
(703) 274-7633
The general public can order copies through the National Technical Information
Service:
NTIS, Commerce Dept.
5285 Port Royal Rd.
Springfield, VA 22161
(703) 487-4650
Electronic copies are available for downloading from the Ada IC Bulletin
Board, (703) 614-0215, and from the AJPO host (ajpo.sei.cmu.edu) on the
Defense Data Network.
AUTHORS AND ACKNOWLEDGEMENTS
The editor for this third version of Ada Quality and Style: Guidelines for
Professional Programmers is Doug Smith. Kent Johnson managed the update
project and the DoD Ada Joint Program Office provided the funding. As part of
this update, a panel of distinguished reviewers was chosen from academia,
government, and industry. Their contributions and efforts to improve this
document are greatly appreciated:
Ms. Christine Braun GTE Federal Systems
Dr. Charles B. Engle, Jr. Department of Computer Science
Florida Institute of Technology
Dr. Michael B. Feldman Department of Electrical Engineering and
Computer Science
George Washington University
Dr. Robert Firth Software Engineering Institute
Carnegie-Mellon University
Ms. Eileen S. Quann FASTRAK Training, Inc.
Dr. Charles H. Sampson Computer Sciences Corporation
Mr. Ed Seidewitz NASA, Goddard Space Flight Center
Lisa Finneran, Rick Kirk, and Eric Marshall also served as Software
Productivity Consortium reviewers. Mike Cochran made the majority of changes
to the Concurrency chapter and Alex Blakemore authored or rewrote several
guidelines for this version. Lyn Uzzle helped fix the menu-driven interface
example. Public comment was invited, and Fred J. Roeber and Bob Crispen
provided a considerable amount of suggestions.
Susan Robanos provided technical editing, Debra Morgan provided word
processing, and Tina Medina provided clean proofing.
A special thanks to GrammaTech, Inc. who made their Ada#ASSURED product
available. All of the examples were formatted in whole or in part using their
tool.
This version builds on the success of the authors and contributors to previous
versions. The acknowledgements from those efforts are included here.
The authors for the second edition were Kent Johnson, Elisa Simmons, and Fred
Stluka. Contributors were Alex Blakemore and Robert Hofkin. Reviewers included
Alex Blakemore, Rick Conn, Tim Harrison, Dave Nettles, and Doug Smith.
Additional support was provided by Vicki Clatterbuck and Leslie Hubbard.
The following people contributed to an instantiation of the first edition's
guidelines: Richard Bechtold, Pete Bloodgood, Shawna Gregory, Tim Powell, Dave
Nettles, Kevin Schaan, Doug Smith, and Perry Tsacoumis.
Special thanks are extended to Loral for providing feedback in the form of
their Software Productivity Laboratory Ada Standards.
The Consortium would also like to acknowledge those involved in the first
edition. The authors were Richard Drake, Samuel Gregory, Margaret Skalko, and
Lyn Uzzle. Paul Cohen managed the project. The contributors and reviewers were
Mark Dowson, John Knight, Henry Ledgard, and Robert Mathis.
Additional supporters included Bruce Barnes, Alex Blakemore, Terry Bollinger,
Charles Brown, Neil Burkhard, William Carlson, Susan Carroll, John
Chludzinski, Vicki Clatterbuck, Robert Cohen, Elizabeth Comer, Daniel Cooper,
Jorge Diaz-Herrera, Tim Harrison, Robert Hofkin, Allan Jaworski, Edward Jones,
John A.N. Lee, Eric Marshall, Charles Mooney, John Moore, Karl Nyberg, Arthur
Pyster, Samuel Redwine, Jr., William Riddle, Lisa Smith, Fred Stluka, Kathy
Velick, David Weiss, and Howard Yudkin.
CONTENTS
CHAPTER 1 Introduction 1
1.1 HOW TO USE THIS BOOK 2
1.2 TO THE NEW Ada PROGRAMMER 2
1.3 TO THE EXPERIENCED Ada PROGRAMMER 3
1.4 TO THE SOFTWARE PROJECT MANAGER 3
1.5 TO CONTRACTING AGENCIES AND STANDARDS ORGANIZATIONS 4
CHAPTER 2 Source Code Presentation 5
2.1 CODE FORMATTING 5
2.2 SUMMARY 15
CHAPTER 3 Readability 17
3.1 SPELLING 17
3.2 NAMING CONVENTIONS 20
3.3 COMMENTS 24
3.4 USING TYPES 35
3.5 SUMMARY 37
CHAPTER 4 Program Structure 41
4.1 HIGH-LEVEL STRUCTURE 41
4.2 VISIBILITY 46
4.3 EXCEPTIONS 50
4.4 SUMMARY 52
CHAPTER 5 Programming Practices 55
5.1 OPTIONAL PARTS OF THE SYNTAX 55
5.2 PARAMETER LISTS 58
5.3 TYPES 60
5.4 DATA STRUCTURES 63
5.5 EXPRESSIONS 66
5.6 STATEMENTS 69
5.7 VISIBILITY 77
5.8 USING EXCEPTIONS 80
5.9 ERRONEOUS EXECUTION 83
5.10 SUMMARY 87
CHAPTER 6 Concurrency 91
6.1 TASKING 91
6.2 COMMUNICATION 96
6.3 TERMINATION 104
6.4 SUMMARY 107
CHAPTER 7 Portability 109
7.1 FUNDAMENTALS 110
7.2 NUMERIC TYPES AND EXPRESSIONS 113
7.3 STORAGE CONTROL 116
7.4 TASKING 117
7.5 EXCEPTIONS 118
7.6 REPRESENTATION CLAUSES AND IMPLEMENTATION#DEPENDENT
FEATURES 119
7.7 INPUT/OUTPUT 122
7.8 SUMMARY 124
CHAPTER 8 Reusability 127
8.1 UNDERSTANDING AND CLARITY 128
8.2 ROBUSTNESS 130
8.3 ADAPTABILITY 136
8.4 INDEPENDENCE 147
8.5 SUMMARY 151
CHAPTER 9 Performance 153
9.1 IMPROVING EXECUTION SPEED 153
9.2 SUMMARY 156
CHAPTER 10 Complete Examples 157
10.1 MENU-DRIVEN USER INTERFACE 157
10.2 LINE-ORIENTED PORTABLE DINING PHILOSOPHERS EXAMPLE 165
10.3 WINDOW-ORIENTED PORTABLE DINING PHILOSOPHERS EXAMPLE 170
APPENDIX A Map from Ada Language Reference Manual to Guidelines 179
REFERENCES 185
BIBLIOGRAPHY 189
INDEX 193
CHAPTER 1
Introduction
This book is intended to help the computer professional produce better Ada
programs. It presents a set of specific guidelines for using the powerful
features of Ada in a disciplined manner. Each guideline consists of a concise
statement of the principles that should be followed, and a rationale
explaining why following the guideline is important. In most cases, an example
of the use of the guideline is provided, and in some cases a further example
is included showing the consequences of violating the guideline. Possible
exceptions to the application of the guideline are explicitly noted, and
further explanatory notes, including notes on how the guideline could be
automated by a tool, are provided where appropriate. Many of the guidelines
are specific enough to be adopted as corporate or project programming
standards. Others require a managerial decision on a particular instantiation
before they can be used as standards. In such cases, a sample instantiation is
presented and used throughout the examples. Such instantiations should be
recognized as weaker recommendations than the guidelines themselves. These
issues are discussed in Section of this introduction. Other sections of the
introduction discuss how this book should be used by various software
development personnel.
Ada was designed to support the development of high-quality, reliable,
reusable, and portable software. For a number of reasons, no programming
language can ensure the achievement of these desirable objectives on its own.
For example, programming must be embedded in a disciplined development process
that addresses requirements analysis, design, implementation, verification,
validation, and maintenance in an organized way. The use of the language must
conform to good programming practices based on well established software
engineering principles. This book is intended to help bridge the gap between
these principles and the actual practice of programming in Ada.
Clear, readable, understandable source text eases program evolution,
adaptation, and maintenance. First, such source text is more likely to be
correct and reliable. Second, effective code adaptation is a prerequisite to
code reuse, a technique that has the potential for drastic reductions in
system development cost. Easy adaptation requires a thorough understanding of
the software; this is considerably facilitated by clarity. Finally, since
maintenance (really evolution) is a costly process that continues throughout
the life of a system, clarity plays a major role in keeping maintenance costs
down. Over the entire life cycle, code has to be read and understood far more
often than it is written; the investment of writing readable, understandable
code is thus well worthwhile. Many of the guidelines in this book are designed
to promote clear source text.
There are two main aspects of code clarity: 1) Careful and consistent layout
of the source text on the page or the screen can enhance readability
dramatically; 2) Careful attention to the structure of code can make it easier
to understand. This is true both on the small scale (e.g., by careful choice
of identifier names or by disciplined use of loops) and on the large scale
(e.g., by proper use of packages). These guidelines treat both layout and
structure.
Comments in source text are a controversial issue. There are arguments both
for and against the view that comments enhance readability. The biggest
problem with comments in practice is that people often fail to update them
when the associated source text is changed, thereby making the commentary
misleading. Commentary should be reserved for expressing needed information
that cannot be expressed in code and highlighting cases where there are
overriding reasons to violate one of the guidelines. If possible, source text
should use self-explanatory names for objects and program units; and it should
use simple, understandable program structures so that little additional
commentary is needed. The extra effort in selecting (and entering) appropriate
names and the extra thought needed to design clean and understandable program
structures are fully justified.
Programming texts often fail to discuss overall program structure; Chapter 4
addresses this. The majority of the guidelines in that chapter are concerned
with the application of sound software engineering principles such as
information hiding and separation of concerns. The chapter is neither a
textbook on nor an introduction to these principles; rather, it indicates how
they can be realized using the features of Ada.
A number of other guidelines are particularly concerned with reliability and
portability issues. They counsel avoidance of language features and
programming practices that either depend on properties not defined in Ada or
on properties that may vary from implementation to implementation. Some of
these guidelines, such as the one forbidding dependence on expression
evaluation order, should never be violated. Others may have to be violated in
special situations such as interfacing to other systems. This should only be
done after careful deliberation, and such violations should be prominently
indicated. Performance constraints are often offered as an excuse for unsafe
programming practices; this is rarely a sufficient justification.
Software tools could be used to enforce, encourage, or check conformance to
many of the guidelines. At present, such tools for Ada primarily consist of
code formatters or syntax directed editors. Existing code formatters are often
parameterizable and can be instantiated to lay out code in a way consistent
with many of the guidelines in this book.
This book is intended for those involved in the actual development of software
systems written in Ada. The following subsections discuss how to make the most
effective use of the material presented. Readers with different levels of Ada
experience and different roles in a software project will need to use the book
in different ways. Specific comments to three broad categories of software
development personnel are addressed: inexperienced Ada programmers,
experienced Ada programmers, and software development managers.
1.1 HOW TO USE THIS BOOK
There are a number of ways in which this book can be used: as a reference on
good Ada style; as a comprehensive list of guidelines which will contribute to
better Ada programs; or as a reference work to consult about using specific
features of the language. The book contains many guidelines, some of which are
quite complex. Learning them all at the same time should not be necessary; it
is unlikely that you will be using all the features of the language at once.
However, it is recommended that all programmers (and, where possible, other
Ada project staff) make an effort to read and understand Chapters 2, 3, and 4
and Chapter 5 up to Section 5.7. Some of the material is quite difficult (for
example, Section 4.2 which discusses visibility), but it covers issues which
are fundamental to the effective use of Ada and is important for any software
professional involved in building Ada systems.
The remainder of the book covers relatively specific issues. Exceptions and
erroneous execution is covered at the end of Chapter 5; and tasking,
portability, and reuse is covered in Chapters 6, 7, and 8 respectively. You
should be aware of the content of this part of the book. You may be required
to follow the guidelines presented in it, but you could defer more detailed
study until needed. Meanwhile, it can serve as useful reference material about
specific Ada features; for example, the discussion of floating point numbers
in the chapter on portability.
This book is not intended as an introductory text on Ada or as a complete
manual of the Ada language. It is assumed that you already know the syntax of
Ada and have a rudimentary understanding of the semantics. With such a
background, you should find the guidelines useful, informative, and often
enlightening.
If you are learning Ada you should equip yourself with a comprehensive
introduction to the language such as Barnes (1989) or Cohen (1986). The Ada
Language Reference Manual (Department of Defense 1983) should be regarded as a
crucial companion to this book. The majority of guidelines reference the
sections of the Ada Language Reference Manual that define the language
features being discussed. Appendix A cross references sections of the Ada
Language Reference Manual to the guidelines.
Throughout the book, references are given to other sources of information
about Ada style and other Ada issues. The references are listed at the end of
the book, followed by a bibliography which includes them and other relevant
sources consulted during the book's preparation.
1.2 TO THE NEW Ada PROGRAMMER
At first sight, Ada offers a bewildering variety of features. It is a powerful
tool intended to solve difficult problems, and almost every feature has a
legitimate application in some context. This makes it especially important to
use Ada's features in a disciplined and organized way. The guidelines in this
book forbid the use of few Ada features. Rather, they show how the features
can be systematically deployed to write clear, high-quality programs.
Following the guidelines will make learning Ada easier and help you to master
its apparent complexity. From the beginning, you can write programs that
exploit the best features of the language in the way that the designers
intended.
Programmers experienced in using another programming language are often
tempted to use Ada as if it were their familiar language but with irritating
syntactic differences. This pitfall should be avoided at all costs; it can
lead to convoluted code that subverts exactly those aspects of Ada that make
it so suitable for building high-quality systems. You must learn to "think
Ada"; following the guidelines in this book and reading the examples of their
use will help you to do this as quickly and painlessly as possible.
To some degree, novice programmers learning Ada have an advantage. Following
the guidelines from the beginning helps in developing a clear programming
style that effectively exploits the language. If you are in this category, it
is recommended that you adopt the guidelines for those exercises you perform
as part of learning Ada. Initially, developing sound programming habits by
concentrating on the guidelines themselves, and their supporting examples, is
more important than understanding the rationale for each guideline. Note that
each chapter ends with a summary of the guidelines it contains.
The rationale for many of the guidelines help experienced programmers
understand and accept the suggestions presented in the guideline. Some of the
guidelines themselves are also written for the experienced programmer who must
make engineering tradeoffs. This is especially true in the areas of
portability, reusability, and performance. These more difficult guidelines and
rationale will make you aware of the issues affecting each programming
decision. You can then use that awareness to recognize the engineering
tradeoffs that you will eventually be asked to make when you are the
experienced Ada programmer.
1.3 TO THE EXPERIENCED Ada PROGRAMMER
As an experienced programmer you are already writing code that conforms to
many of the guidelines in this book. In some areas, however, you may have
adopted a personal programming style that differs from that presented here,
and you might be reluctant to change. Carefully review those guidelines that
are inconsistent with your current style, make sure that you understand their
rationale, and consider adopting them. The overall set of guidelines in this
book embodies a consistent approach to producing high-quality programs that
would be weakened by too many exceptions.
Another important reason for general adoption of common guidelines is
consistency. If all the staff of a project write source text in the same
style, many critical project activities are easier. Consistent code simplifies
formal and informal code reviews, system integration, within-project code
reuse, and the provision and application of supporting tools. In practice,
corporate or project standards may require deviations from the guidelines to
be explicitly commented, so adopting a nonstandard approach may require extra
work.
1.4 TO THE SOFTWARE PROJECT MANAGER
Technical management plays a key role in ensuring that the software produced
in the course of a project is correct, reliable, maintainable, and portable.
Management must create a project-wide commitment to the production of
high-quality code; define project-specific coding standards and guidelines;
foster an understanding of why uniform adherence to the chosen coding
standards is critical to product quality; and establish policies and
procedures to check and enforce that adherence. The guidelines contained in
this book can aid such an effort.
An important activity for managers is the definition of coding standards for a
project or organization. These guidelines do not, in themselves, constitute a
complete set of standards; however, they can serve as a basis for them. A
number of guidelines indicate a range of decisions, but they do not prescribe
a particular decision. For example, the second guideline in the book
(Guideline ) advocates using a consistent number of spaces for indentation and
indicates in the rationale that 2 to 4 spaces would be reasonable. With your
senior technical staff, you should review each such guideline and arrive at a
decision about its instantiation that will constitute your project or
organizational standard.
Two other areas require managerial decisions about standardization. Guideline
advises you to avoid arbitrary abbreviations in object or unit names. You
should prepare a glossary of acceptable abbreviations for a project that
allows the use of shorter versions of application-specific terms (e.g., FFT
for Fast Fourier Transform or SPN for Stochastic Petri Net). You should keep
this glossary short and restrict it to terms which need to be used frequently
as part of names. Having to refer continually to an extensive glossary to
understand source text makes it hard to read.
The portability guidelines given in Chapter 7 need careful attention.
Adherence to them is important even if the need to port the resulting software
is not currently foreseen. Following the guidelines improve the potential
reusability of the resulting code in projects that use different Ada
implementations. You should insist that when particular project needs force
the relaxation of some of the portability guidelines, nonportable features of
the source text are prominently indicated. Observing the Chapter 7 guidelines
requires definition and standardization of project- or organization-specific
numeric types to use in place of the (potentially nonportable) predefined
numeric types.
Your decisions on standardization issues should be incorporated in a project
or organization coding standards document.
With coding standards in place, you need to ensure adherence to them. Probably
the most important aspect of this is gaining the wholehearted commitment of
your programming staff to use them. Given this commitment, and the example of
high-quality Ada being produced by your programmers, it will be far easier to
conduct effective formal code reviews that check compliance to project
standards.
Consistent coding standards work well with automatic tool support. If you have
a tools group in your project or organization, they can be tasked to acquire
or develop tools to support your standards. It is very cost effective to use
tools to enforce standards. Where tools cannot be used to automatically modify
code to conform to standards, they can often be used to at least check
conformance. See the automation notes sections associated with many of the
guidelines.
Some general issues concerning the management of Ada projects are discussed by
Foreman and Goodenough (1987).
1.5 TO CONTRACTING AGENCIES AND STANDARDS ORGANIZATIONS
The guidelines in this document are not intended to stand alone as a standard.
It is not even clear in some cases that a guideline could be enforced since it
is only intended to make the engineer aware of tradeoffs. In other cases, a
choice still remains about a guideline, such as how many spaces to use for
each level of indentation.
When a guideline is too general to show an example, the "instantiation"
section of each guideline contains more specific guidelines. These can be
considered for a standard and are more likely to be enforceable. Any
organization that attempts to extract standards from this document needs to
evaluate the complete context. Each guideline works best when related
guideines are practiced. In isolation, a guideline may have little or no
benefit.
CHAPTER 2
Source Code Presentation
The physical layout of source text on the page or screen has a strong effect
on its readability. This chapter contains source code presentation guidelines
intended to make the code more readable.
In addition to the general purpose guidelines, specific recommendations are
made in the "instantiation" sections. If you disagree with the specific
recommendations, you may want to adopt your own set of conventions that still
follow the general purpose guidelines. Above all, be consistent across your
entire project.
An entirely consistent layout is hard to achieve or check manually. Therefore
you may prefer to automate layout with a tool for parameterized code
formatting or incorporate the guidelines into an automatic coding template.
Some of the guidelines and specific recommendations presented in this section
cannot be enforced by a formatting tool because they are based on the
semantics, not the syntax, of the Ada code. More details are given in the
"automation notes" sections.
2.1 CODE FORMATTING
The "code formatting" of Ada source code affects how the code looks, not what
the code does. Topics included here are horizontal spacing, indentation,
alignment, pagination, and line length. The most important guideline is to be
consistent throughout the compilation unit as well as the project.
2.1.1 Horizontal Spacing
guideline
o Use consistent spacing around delimiters.
o Use the same spacing as you would in regular prose.
instantiation
Specifically, leave at least one blank space in the following places, as shown
in the examples throughout this book. More spaces may be required for the
vertical alignment recommended in subsequent guidelines.
- Before and after the following delimiters and binary operators:
+ - * / &
< = > /= <= >=
:= => | ..
:
<>
- Outside of the quotes for string (") and character (') literals,
except where prohibited.
- Outside, but not inside, of parentheses.
- After commas (,) and semicolons (;).
Do not leave any blank spaces in the following places, even if this conflicts
with the above recommendation.
- After the plus (+) and minus (-) signs when used as unary operators.
- After a function call.
- Inside of label delimiters (<< >>).
- Before and after the apostrophe (') and period (.)
- Between multiple consecutive opening or closing parentheses.
- Before commas (,) and semicolons (;).
When superfluous parentheses are omitted because of operator precedence rules,
spaces may optionally be removed around the highest precedence operators in
that expression.
example
Default_String : constant String :=
"This is the long string returned by" &
" default. It is broken into multiple" &
" Ada source lines for convenience.";
type Signed_Whole_16 is range -2**15 .. 2**15 - 1;
type Address_Area is array (Natural range <>) of Signed_Whole_16;
Register : Address_Area (16#7FF0# .. 16#7FFF#);
Memory : Address_Area ( 0 .. 16#7FEC#);
Register(Pc) := Register(A);
X := Signed_Whole_16(Radius * Sin(Angle));
Register(Index) := Memory(Base_Address + Index * Element_Length);
Get(Value => Sensor);
Error_Term := 1.0 - (Cos(Theta)**2 + Sin(Theta)**2);
Z := X**3;
Y := C * X + B;
Volume := Length * Width * Height;
rationale
It is a good idea to use white space around delimiters and operators because
they are typically short (one or two character) sequences that can easily get
lost among the longer keywords and identifiers. Putting white space around
them makes them stand out. Consistency in spacing also helps make the source
code easier to scan visually.
However, many of the delimiters (commas, semicolons, parentheses, etc.) are
familiar as normal punctuation marks. It is distracting to see them spaced
differently in a computer program than in normal text. Therefore, they should
be spaced the same (no spaces before commas and semicolons, no spaces inside
of parentheses, etc.).
exception
The one notable exception is the colon (:). In Ada, it is useful to use the
colon as a tabulator or a column separator (see Guideline ). In this context,
it makes sense to put spaces before and after the colon, rather than only
after as in normal text.
automation notes
The guidelines in this section are easily enforced with an automatic code
formatter.
2.1.2 Indentation
guideline
o Indent and align nested control structures, continuation lines, and
embedded units consistently.
o Distinguish between indentation for nested control structures and for
continuation lines.
o Use spaces for indentation, not the tab character (Nissen and Wallis
1984, w2.2).
instantiation
Specifically, the following indentation conventions are recommended, as shown
in the examples throughout this book. Note that the minimum indentation is
described. More spaces may be required for the vertical alignment recommended
in subsequent guidelines.
- Use the recommended paragraphing shown in the Ada Language Reference
Manual (Department of Defense 1983).
- Use three spaces as the basic unit of indentation for nesting.
- Use two spaces as the basic unit of indentation for continuation lines.
A label is outdented three spaces. A continuation line is indented two spaces:
begin
<<label>> | <long statement with line break>
<statement> | <trailing part of same statement>
end;
The if statement and the plain loop:
if <condition> then | <name>:
<statements> | loop
elsif <condition> then | <statements>
<statements> | exit when <condition>;
else | <statements>
<statements> | end loop <name>;
end if; |
Loops with the for and while iteration schemes:
<name>: | <name>:
for <scheme> loop | while <condition> loop
<statements> | <statements>
end loop <name>; | end loop <name>;
The block and the case statement as recommended in the Ada Language Reference
Manual (Department of Defense 1983):
<name>: | case <expression> is
declare | when <choice> =>
<declarations> | <statements>
begin | when <choice> =>
<statements> | <statements>
exception | when others =>
when <choice> => | <statements>
<statements> | end case; --<comment>
when others => |
<statements> |
end <name>; |
These case statements save space over the the Ada Language Reference Manual
(Department of Defense 1983) recommendation and depend on very short statement
lists, respectively. Whichever you choose, be consistent.
case <expression> is | case <expression> is
when <choice> => | when <choice> => <statements>
<statements> | <statements>
when <choice> => | when <choice> => <statements>
<statements> | when others => <statements>
when others => | end case;
<statements> |
end case; |
The various forms of selective wait and the timed and conditional entry calls:
select | select
when <guard> => | <entry call>;
<accept statement> | <statements>
<statements> | or
or | delay <interval>;
<accept statement> | <statements>
<statements> | end select;
or |
when <guard> => |
delay <interval>; |
<statements> |
or | select
when <guard> => | <entry call>;
terminate; | <statements>
else | else
<statements> | <statements>
end select; | end select;
The accept statement and a subunit:
accept <specification> do | separate (<parent unit>)
<statements> | <proper body>
end <name>; |
Proper bodies of program units:
procedure <specification> is | package body <name> is
<declarations> | <declarations>
begin | begin
<statements> | <statements>
exception | exception
when <choice> => | when <choice> =>
<statements> | <statements>
end <name>; | end <name>;
|
function <specification> | task body <name> is
return <type name> is | <declarations>
<declarations> | begin
begin | <statements>
<statements> | exception
exception | when <choice> =>
when <choice> => | <statements>
<statements> | end <name>;
end <name>; |
Context clauses on compilation units are arranged as a table. Generic formal
parameters do not obscure the unit itself. Function, package, and task
specifications use standard indentation:
with <name>; | function <specification>
with <name>; | return <type>;
with <name>; |
| package <name> is
use <name>; | <declarations>
| private
<compilation unit> | <declarations>
| end <name>;
|
generic | task type <name> is
<formal parameters> | <entry declarations>
<compilation unit> | end <name>;
Instantiations of generic units and record indentation:
procedure <name> is |type ... is
new <generic name> <actuals> | record
| <component list>
function <name> is | case <discriminant name> is
new <generic name> <actuals>| when <choice> =>
| <component list>
package <name> is | when <choice> =>
new <generic name> <actuals> | <component list>
| end case;
| end record;
Indentation for record alignment:
for <name> use
record <alignment clause>
<component clause>
end record;
example
Default_String : constant String :=
"This is the long string returned by" &
" default. It is broken into multiple" &
" Ada source lines for convenience.";
loop
if Input_Found then
Count_Characters;
else --not Input_Found
Reset_State;
Character_Total :=
First_Part_Total * First_Part_Scale_Factor +
Second_Part_Total * Second_Part_Scale_Factor +
Default_String'Length + Delimiter_Size;
end if;
end loop;
rationale
Indentation improves the readability of the code because it gives the reader a
visual indicator of the program structure. The levels of nesting are clearly
identified by indentation and the first and last keywords in a construct can
be matched visually.
While there is much discussion on the number of spaces to indent, the reason
for indentation is code clarity. The fact that the code is indented
consistently is more important than the number of spaces used for indentation.
Additionally, in Section 1.5, the Ada Language Reference Manual says that the
layout shown in the examples and syntax rules in the Ada Language Reference
Manual is the recommended code layout to be used for Ada programs. "The syntax
rules describing structured constructs are presented in a form that
corresponds to the recommended paragraphing. ... Different lines are used for
parts of a syntax rule if the corresponding parts of the construct described
by the rule are intended to be on different lines. ... It is recommended that
all indentation be by multiples of a basic step of indentation (the number of
spaces for the basic step is not defined)."
It is important to indent continuation lines differently from nested control
structures to make them visually distinct. This prevents them from obscuring
the structure of the code as the user scans it.
Indenting with spaces is more portable than indenting with tabs because tab
characters are displayed differently by different terminals and printers.
automation notes
The guidelines in this section are easily enforced with an automatic code
formatter.
2.1.3 Alignment of Operators
guideline
o Align operators vertically to emphasize local program structure and
semantics.
example
if Slot_A >= Slot_B then
Temporary := Slot_A;
Slot_A := Slot_B;
Slot_B := Temporary;
end if;
Numerator := B**2 - 4.0 * A * C;
Denominator := 2.0 * A;
Solution_1 := -B + Square_Root(Numerator / Denominator);
Solution_2 := B + Square_Root(Numerator / Denominator);
X := A * B +
C * D +
E * F;
Y := (A * B + C) + -- basic equation
(2.0 * D - E) - --
3.5; -- account for error factor
rationale
Alignment makes it easier to see the position of the operators and, therefore,
puts visual emphasis on what the code is doing.
The use of lines and spacing on long expressions can emphasize terms,
precedence of operators, and other semantics. It can also leave room for
highlighting comments within an expression.
exceptions
If vertical alignment of operators forces a statement to be broken over two
lines, and especially if the break is at an inappropriate spot, it may be
preferable to relax the alignment guideline.
automation notes
The last example above shows a type of "semantic alignment" which is not
typically enforced or even preserved by automatic code formatters. If you
break expressions into semantic parts and put each on a separate line, beware
of using a code formatter later. It is likely to move the entire expression to
a single line and accumulate all the comments at the end. However, there are
some formatters which are intelligent enough to leave a line break intact when
the line contains a comment. A good formatter will recognize that the last
example above does not violate the guidelines and would therefore preserve it
as written.
2.1.4 Alignment of Declarations
guideline
o Use vertical alignment to enhance the readability of declarations.
o Provide at most one declaration per line.
o Indent all declarations in a single declarative part at the same level.
instantiation
For declarations not separated by blank lines, follow these alignment rules.
- Align the colon delimeters.
- Align the initialization delimeter, ":=".
- When trailing comments are used, align the comment delimeter.
- When the declaration overflows a line, break the line and add an
indentation level for those lines that wrap. The preferred places to break,
in order are: 1) The comment delimeter; 2) The initialization delimeter; 3)
The colon delimeter.
- For enumeration type declarations which do not fit on a single line,
put each literal on a separate line, using the next level of indentation.
When appropriate, semantically related literals can be arranged by row or
column to form a table.
example
Variable and constant declarations can be laid out in a table with columns
separated by the symbols :, :=, and --
Prompt_Column : constant := 40;
Question_Mark : constant String := " ? "; -- prompt on error input
Prompt_String : constant String := " ==> ";
If this results in lines that are too long, they can be laid out with each
part on a separate line with its unique indentation level.
subtype User_Response_Text_Frame is String (1 .. 72);
-- If the declaration needed a comment, it would fit here.
Input_Line_Buffer : User_Response_Text_Frame
:= Prompt_String &
String'(1 .. User_Response_Text_Frame'Length -
Prompt_String'Length => ' ');
Declarations of enumeration literals can be listed in one or more columns as:
type Op_Codes_In_Column is
(Push,
Pop,
Add,
Subtract,
Multiply,
Divide,
Subroutine_Call,
Subroutine_Return,
Branch,
Branch_On_Zero,
Branch_On_Negative);
or, to save space:
type Op_Codes_Multiple_Columns is
(Push, Pop, Add,
Subtract, Multiply, Divide,
Subroutine_Call, Subroutine_Return, Branch,
Branch_On_Zero, Branch_On_Negative);
or, to emphasize related groups of values:
type Op_Codes_In_Table is
(Push, Pop,
Add, Subtract, Multiply, Divide,
Subroutine_Call, Subroutine_Return,
Branch, Branch_On_Zero, Branch_On_Negative);
rationale
Many programming standards documents require tabular repetition of names,
types, initial values, and meaning in unit header comments. These comments are
redundant and can become inconsistent with the code. Aligning the declarations
themselves in tabular fashion (see the examples above) provides identical
information to both compiler and reader, enforces at most one declaration per
line, and eases maintenance by providing space for initializations and
necessary comments. A tabular layout enhances readability, thus preventing
names from "hiding" in a mass of declarations. This applies to type
declarations as well as object declarations.
automation notes
Most of the guidelines in this section are easily enforced with an automatic
code formatter. The one exception is the last enumerated type example, which
is laid out in rows based on the semantics of the enumeration literals. An
automatic code formatter will not be able to do this, and will likely move the
enumeration literals to different lines. However, tools that are only
checking for violations of the guidelines should accept the tabular form of an
enumeration type declaration.
2.1.5 More on Alignment
guideline
o Align parameter modes and parentheses vertically.
instantiation
Specifically it is recommended that you:
- Place one formal parameter specification per line.
12 Ada QUALITY AND STYLE
- Vertically align parameter names, colons, the reserved word in, the
reserved word out, and parameter types.
- Place the first parameter specification on the same line as the
subprogram or entry name. If any of the parameter types are forced beyond the
line length limit, place the first parameter specification on a new line
indented as for continuation lines.
example
procedure Display_Menu (Title : in String;
Options : in Menus;
Choice : out Alpha_Numerics);
or
procedure Display_Menu_On_Primary_Window
(Title : in String;
Options : in Menus;
Choice : out Alpha_Numerics);
or
procedure Display_Menu_On_Screen (
Title : in String;
Options : in Menus;
Choice : out Alpha_Numerics
);
Aligning parentheses makes complicated relational expressions more clear:
if not (First_Character in Alpha_Numerics and then
Valid_Option(First_Character)) then
rationale
This facilitates readability and understandability. Aligning parameter modes
provides the effect of a table with columns for parameter name, mode, type,
and, if necessary, parameter-specific comments. Vertical alignment of
parameters across subprograms within a compilation unit increases the
readability even more.
note
Various options are available for subprogram layout. The second example above
aligns all of the subprogram names and parameter names in a program.This has
the disadvantage of occupying an unnecessary line where subprogram names are
short and looking awkward if there is only one parameter.
The third example is a format commonly used to reduce the amount of editing
required when parameter lines are added, deleted, or reordered. The
parentheses don't have to be moved from line to line. However, the last
parameter line is the only one without a semicolon.
automation notes
Most of the guidelines in this section are easily enforced with an automatic
code formatter. The one exception is the last example, which shows vertical
alignment of parentheses to emphasize terms of an expression. This is
difficult to achieve with an automatic code formatter unless the relevant
terms of the expression can be determined strictly through operator
precedence.
2.1.6 Blank Lines
guideline
o Use blank lines to group logically related lines of text (NASA 1987).
example
if ... then
for ... loop
...
end loop;
end if;
This example separates different kinds of declarations with blank lines:
type Employee_Record is
record
Legal_Name : Name;
Date_Of_Birth : Date;
Date_Of_Hire : Date;
Salary : Money;
end record;
type Day is
(Monday, Tuesday, Wednesday, Thursday, Friday,
Saturday, Sunday);
subtype Weekday is Day range Monday .. Friday;
subtype Weekend is Day range Saturday .. Sunday;
rationale
When blank lines are used in a thoughtful and consistent manner, sections of
related code are more visible to readers.
automation notes
Automatic formatters do not enforce this guideline well because the decision
on where to insert blank lines is a semantic one. However, many formatters
have the ability to leave existing blank lines intact. Thus, you can manually
insert the lines and not lose the effect when you run such a formatter.
2.1.7 Pagination
guideline
o Highlight the top of each package or task specification, the top of
each program unit body, and the end statement of each program unit.
instantiation
Specifically, it is recommended that you:
- Use file prologues, specification headers, and body headers to
highlight those structures as recommended in Guideline .
- Use a line of dashes, beginning at the same column as the current
indentation to highlight the definition of subunits embedded in a declarative
part. Insert the line of dashes immediately before and immediately after the
definition.
- If two dashed lines are adjacent, omit the longer of the two.
example
with Basic_Types;
package body SPC_Numeric_Types is
...
---------------------------------------------------------------------
function Max
(Left : in Basic_Types.Tiny_Integer;
Right : in Basic_Types.Tiny_Integer)
return Basic_Types.Tiny_Integer is
begin
if Right < Left then
return Left;
else
return Right;
end if;
end Max;
---------------------------------------------------------------------
function Min
(Left : in Basic_Types.Tiny_Integer;
Right : in Basic_Types.Tiny_Integer)
return Basic_Types.Tiny_Integer is
begin
if Left < Right then
return Left;
else
return Right;
end if;
end Min;
---------------------------------------------------------------------
use Basic_Types;
begin -- SPC_Numeric_Types
Max_Tiny_Integer := Min(System_Max, Local_Max);
Min_Tiny_Integer := Max(System_Min, Local_Min);
-- ...
end SPC_Numeric_Types;
rationale
It is easy to overlook parts of program units that are not visible on the
current page or screen. The page lengths of presentation hardware and software
vary widely. By clearly marking the program's logical page boundaries (e.g.,
with a dashed line), you enable a reader to quickly check whether all of a
program unit is visible. Such pagination also makes it easier to scan a large
file quickly, looking for a particular program unit.
note
This guideline does not address code layout on the physical "page" because the
dimensions of such pages vary widely and no single guideline is appropriate.
automation notes
The guidelines in this section are easily enforced with an automatic code
formatter.
2.1.8 Number of Statements Per Line
guideline
o Start each statement on a new line.
o Write no more than one simple statement per line.
o Break compound statements over multiple lines.
example
Use
if End_Of_File then
Close_File;
else
Get_Next_Record;
end if;
rather than
if End_Of_File then Close_File; else Get_Next_Record; end if;
exceptional case
Put("A="); Natural_IO.Put(A); New_Line;
Put("B="); Natural_IO.Put(B); New_Line;
Put("C="); Natural_IO.Put(C); New_Line;
rationale
A single statement on each line enhances the reader's ability to find
statements and helps prevent statements being missed. Similarly, the structure
of a compound statement is clearer when its parts are on separate lines.
note
If a statement is longer than the remaining space on the line, continue it on
the next line. This guideline includes declarations, context clauses, and
subprogram parameters.
According to the Ada Language Reference Manual (Department of Defense 1983),
"The preferred places for other line breaks are after semicolons."
exceptions
The example of Put and Newline statements shows a legitimate exception. This
grouping of closely related statements on the same line makes the structural
relationship between the groups clear.
automation notes
The guidelines in this section are easily enforced with an automatic code
formatter, with the single exception of the last example which shows a
semantic grouping of multiple statements onto a single line.
2.1.9 Source Code Line Length
guideline
o Adhere to a maximum line length limit for source code (Nissen and
Wallis 1984, w2.3).
instantiation
Specifically, it is recommended that you:
- Limit source code line lengths to a maximum of 72 characters.
rationale
When Ada code is ported from one system to another, there may be restrictions
on the record size of source line statements possibly for one of the following
reasons: some operating systems may not support variable length records for
tape I/O or some printers and terminals support an 80-character line width with
no line-wrap. See further rationale in the note for Guideline .
Source code must sometimes be published for various reasons, and letter-size
paper is not as forgiving as a computer listing in terms of the number of
usable columns.
In addition, there are human limitations in the width of the field of view for
understanding at the level required for reading source code. These limitations
correspond roughly to the 70 to 80 column range.
automation notes
The guidelines in this section are easily enforced with an automatic code
formatter.
2.2 SUMMARY
code formatting
o Use consistent spacing around delimiters.
o Use the same spacing as you would in regular prose.
o Indent and align nested control structures, continuation lines, and
embedded units consistently.
o Distinguish between indentation for nested control structures and for
continuation lines.
o Use spaces for indentation, not the tab character (Nissen and Wallis
1984, w2.2).
o Align operators vertically to emphasize local program structure and
semantics.
o Use vertical alignment to enhance the readability of declarations.
o Provide at most one declaration per line.
o Indent all declarations in a single declarative part at the same level.
o Align parameter modes and parentheses vertically.
o Use blank lines to group logically related lines of text (NASA 1987).
o Highlight the top of each package or task specification, the top of
each program unit body, and the end statement of each program unit.
o Start each statement on a new line.
o Write no more than one simple statement per line.
o Break compound statements over multiple lines.
o Adhere to a maximum line length limit for source code (Nissen and
Wallis 1984, 2.3).
CHAPTER 3
Readability
This chapter recommends ways of using Ada features to make reading and
understanding code easier. There are many myths about comments and
readability. The responsibility for true readability rests more with naming
and code structure than with comments. Having as many comment lines as code
lines does not imply readability; it more likely indicates the writer does not
understand what is important to communicate.
3.1 SPELLING
Spelling conventions in source code include rules for capitalization, use of
underscores, and use of abbreviations. If these conventions are followed
consistently, the resulting code is clearer and more readable.
3.1.1 Use of Underscores
guideline
o Use underscores to separate words in a compound name.
example
Miles_Per_Hour
Entry_Value
rationale
When an identifier consists of more than one word, it is much easier to read
if the words are separated by underscores. Indeed, there is precedent in
English in which compound words are separated by a hyphen. In addition to
promoting readability of the code, if underscores are used in names, a code
formatter has more control over altering capitalization. See Guideline .
3.1.2 Numbers
guideline
o Represent numbers in a consistent fashion.
o Represent literals in a radix appropriate to the problem.
o Use underscores to separate digits the same way commas or periods (or
spaces for nondecimal bases) would be used in handwritten text.
o When using scientific notation, make the E consistently either upper
or lower case.
o In an alternate base, represent the alphabetic characters in either
all upper case or all lower case.
instantiation
- Decimal and octal numbers are grouped by threes beginning counting on
either side of the radix point.
- The E is always capitalized in scientific notation.
- Use upper case for the alphabetic characters representing digits in
bases above 10.
- Hexadecimal numbers are grouped by fours beginning counting on either
side of the radix point.
example
type Maximum_Samples is range 1 .. 1_000_000;
type Legal_Hex_Address is range 16#0000# .. 16#FFFF#;
type Legal_Octal_Address is range 8#000_000# .. 8#777_777#;
Avogadro_Number : constant := 6.022_169E+23;
To represent the number 1/3 as a constant, use
One_Third : constant := 1.0 / 3.0;
Avoid this use.
One_Third_As_Decimal_Approximation : constant := 0.333_333_333_333_33;
or
One_Third_Base_3 : constant := 3#0.1#; -- Yes, it really works!
rationale
Consistent use of upper case or lower case aids scanning for numbers.
Underscores serve to group portions of numbers into familiar patterns.
Consistency with common use in everyday contexts is a large part of
readability.
note
If a rational fraction is represented in a base in which it has a terminating
rather than repeating representation, as 3#0.1# does in the example above, it
may have increased accuracy upon conversion to the machine base.
3.1.3 Capitalization
guideline
o Make reserved words and other elements of the program visually
distinct from each other.
instantiation
- Use lower case for all reserved words (when used as reserved words).
- Use mixed case for all other identifiers, a capital letter beginning
every word separated by underscores.
- Use upper case for abbreviations and acronyms (see automation note).
example
...
type Second_Of_Day is range 0 .. 86_400;
type Noon_Relative_Time is (Before_Noon, After_Noon, High_Noon);
subtype Morning is Second_Of_Day range 0 .. 86_400 / 2 - 1;
subtype Afternoon is Second_Of_Day range Morning'Last + 2 .. 86_400;
...
Current_Time := Second_Of_Day(Calendar.Seconds(Calendar.Clock));
if Current_Time in Morning then
Time_Of_Day := Before_Noon;
elsif Current_Time in Afternoon then
Time_Of_Day := After_Noon;
else
Time_Of_Day := High_Noon;
end if;
case Time_Of_Day is
when Before_Noon => Get_Ready_For_Lunch;
when High_Noon => Eat_Lunch;
when After_Noon => Get_To_Work;
end case;
...
rationale
Visually distinguishing reserved words allows the reader to focus on program
structure alone, if desired, and also aids scanning for particular identifiers.
The instantiation chosen here is meant to be more readable for the experienced
Ada programmer, who does not need reserved words to leap off the page.
Beginners to any language often find that reserved words should be emphasized
to help them find the control structures more easily. Because of this,
instructors in the classroom and books introducing the Ada language may want
to consider an alternative instantiation. It should be instructive to note
that the Ada Language Reference Manual chose to bold all reserved words.
Upper case for reserved words may also be suitable.
note
In Section 2.1, Nissen and Wallis (1984) states that "The choice of case is
highly debatable, and that chosen for the [Ada Language Reference Manual
(Department of Defense 1983)] is not necessarily the best. The use of lower
case for reserved words is often preferred, so that they do not stand out too
much. However, lower case is generally easier to read than upper case; words
can be distinguished by their overall shape, and can be found more quickly
when scanning the text."
automation note
Ada names are not case sensitive. Therefore the names max_limit, MAX_LIMIT,
and Max_Limit denote the same object or entity. A good code formatter should
be able to automatically convert from one style to another as long as the
words are delimited by underscores.
As recommended in Guideline , abbreviations should be project wide. An
automated tool should allow a project to specify those abbreviations and
format them accordingly.
3.1.4 Abbreviations
guideline
o Do not use an abbreviation of a long word as an identifier where a
shorter synonym exists.
o Use a consistent abbreviation strategy.
o Do not use ambiguous abbreviations.
o An abbreviation must save many characters over the full word to be
justified.
o Use abbreviations that are well-accepted in the application domain.
o Maintain a list of accepted abbreviations and use only abbreviations
on that list.
example
Use Time_Of_Receipt rather than Recd_Time or R_Time.
But in an application that commonly deals with message formats that meet
military standards, DOD_STD_MSG_FMT is an acceptable abbreviation for:
Department_Of_Defense_Standard_Message_Format.
rationale
Many abbreviations are ambiguous or unintelligible unless taken in context. As
an example, Temp could indicate either temporary or temperature. For this
reason, you should choose abbreviations carefully when you use them. The
rationale in Guideline provides a more thorough discussion of how context
should influence the use of abbreviations.
Since very long variable names can obscure the structure of the program,
especially in deeply nested (indented) control structures, it is a good idea
to try to keep identifiers short and meaningful. Use short unabbreviated names
whenever possible. If there is no short word which will serve as an
identifier, then a well known unambiguous abbreviation is the next best
choice, especially if it comes from a list of standard abbreviations used
throughout the project.
An abbreviated format for a fully qualified name can be established via the
renames clause. This capability is useful when a very long, fully qualified
name would otherwise occur many times in a localized section of code (see
Guideline ).
A list of accepted abbreviations for a project provides a standard context for
using each abbreviation.
3.2 NAMING CONVENTIONS
Choose names that clarify the object's or entity's intended use. Ada allows
identifiers to be any length as long as the identifier fits on a line with all
characters being significant (including underscores). Identifiers are the
names used for variables, constants, program units, and other entities within
a program.
3.2.1 Names
guideline
o Choose names that are as self-documenting as possible.
o Use a short synonym instead of an abbreviation (see Guideline 3.1.4).
o Use names given by the application but not obscure jargon.
example
In a tree-walker, using the name Left instead of Left_Branch is sufficient to
convey the full meaning given the context. However, use Time_Of_Day instead
of TOD.
Mathematical formulas are often given using single-letter names for variables.
Continue this convention for mathematical equations where it would recall the
formula; for example:
A*(X**2) + B*X + C.
rationale
A program that follows these guidelines can be more easily comprehended.
Self-documenting names require fewer explanatory comments. Empirical studies
have shown that you can further improve comprehension if your variable names
are not excessively long (Schneiderman 1986, 7). The context and application
can help greatly. The unit of measure for numeric entities can be a source of
type names.
note
See Guideline for a discussion on how to use the application domain as a
guideline for selecting abbreviations.
3.2.2 Type Names
guideline
o Use singular, general nouns as (sub)type identifiers.
o Choose identifiers that describe one of the (sub)type's values.
o Do not use identifier constructions (e.g., suffixes) that are unique
to (sub)type identifiers.
o Do not use the type names from predefined packages.
example
type Day is
(Monday, Tuesday, Wednesday, Thursday, Friday,
Saturday, Sunday);
type Day_Of_Month is range 0 .. 31;
type Month_Number is range 1 .. 12;
type Historical_Year is range -6_000 .. 2_500;
type Date is
record
Day : Day_Of_Month;
Month : Month_Number;
Year : Historical_Year;
end record;
In particular, Day should be used in preference to Days or Day_Type;
The identifier Historical_Year might appear to be specific, but it is actually
general, with the adjective, historical, describing the range constraint.
rationale
When this style and the suggested style for object identifiers are used,
program code more closely resembles English (see Guideline 3.2.3).
Furthermore, this style is consistent with the names of the language's
predefined identifiers. They are not named Integers, Booleans, Integer_Type,
or Boolean_Type.
However, using the name of a type from the predefined packages is sure to
confuse a programmer when that type appears somewhere without a package
qualification.
3.2.4 Object Names
guideline
o Use predicate clauses or adjectives for boolean objects.
o Use singular, specific nouns as object identifiers.
o Choose identifiers that describe the object's value during execution.
o Use singular, general nouns as identifiers for record components.
example
Nonboolean objects:
Today : Day;
Yesterday : Day;
Retirement_Date : Date;
Boolean objects:
User_Is_Available : Boolean; -- predicate clause
List_Is_Empty : Boolean; -- predicate clause
Empty : Boolean; -- adjective
Bright : Boolean; -- adjective
rationale
Using specific nouns for objects establishes a context for understanding the
object's value, which is one of the general values described by the
(sub)type's name (Guideline 3.2.2). Object declarations become very
English-like with this style. For example, the first declaration above is
read as "Today is a Day."
General nouns, rather that specific, are used for record components because a
record object's name will supply the context for understanding the component.
Thus, the following component is understood as "the year of retirement.":
Retirement_Date.Year
Following conventions which relate object types and parts of speech makes code
read more like text. For example, because of the names chosen, the following
code segment needs no comments:
if List_Is_Empty then
Number_Of_Elements := 0;
else
Number_Of_Elements := Length_Of_List;
end if;
note
If it is difficult to find a specific noun that describes an object's value
during the entire execution of a program, the object is probably serving
multiple purposes. Multiple objects should be used in such a case.
3.2.4 Program Unit Names
guideline
o Use action verbs for procedures and entries.
o Use predicate-clauses for boolean functions.
o Use nouns for nonboolean functions.
o Give packages names that imply higher levels of organization than
subprograms. Generally, these are noun phrases that describe the abstraction
provided.
o Give tasks names that imply an active entity.
o Name generic subprograms as if they were nongeneric subprograms.
o Name generic packages as if they were nongeneric packages.
o Make the generic names more general than the instantiated names.
example
The following are sample names for elements that comprise an Ada program.
Sample procedure names:
procedure Get_Next_Token -- get is a transitive verb
procedure Create -- create is a transitive verb
Sample function names for boolean-valued functions:
function Is_Last_Item -- predicate clause
function Is_Empty -- predicate clause
Sample function names for nonboolean-valued functions:
function Successor -- common noun
function Length -- attribute
function Top -- component
Sample package names:
package Terminal is -- common noun
package Text_Utilities is -- common noun
Sample task names:
task Terminal_Resource_Manager is -- common noun that shows action
Below is a sample piece of code to show the clarity that results from using
these conventions the parts-of-speech naming conventions.
Get_Next_Token(Current_Token);
case Current_Token is
when Identifier => Process_Identifier;
when Numeric => Process_Numeric;
end case; -- Current_Token
if Is_Empty(Current_List) then
Number_Of_Elements := 0;
else
Number_Of_Elements := Length(Current_List);
end if;
When packages and their subprograms are named together, the resulting code is
very descriptive.
if Stack.Is_Empty(Current_List) then
Current_Token := Stack.Top(Current_List);
end if;
rationale
Using these naming conventions creates understandable code that reads much
like natural language. When verbs are used for actions, such as subprograms,
and nouns are used for objects, such as the data that the subprogram
manipulates, code is easier to read and understand. This models a medium of
communication already familiar to a reader. Where the pieces of a program
model a real-life situation, using these conventions reduces the number of
translation steps involved in reading and understanding the program. In a
sense, your choice of names reflects the level of abstraction from computer
hardware toward application requirements.
note
There are some conflicting conventions in current use for task entries. Some
programmers and designers advocate naming task entries with the same
conventions used for subprograms to blur the fact that a task is involved.
Their reasoning is that if the task is reimplemented as a package, or vice
versa, the names need not change. Others prefer to make the fact of a task
entry as explicit as possible to ensure that the existence of a task with its
presumed overhead is recognizable. Project-specific priorities may be useful in
choosing between these conventions.
3.2.5 Constants and Named Numbers
guideline
o Use symbolic values instead of literals wherever possible.
o Use constants instead of variables for constant values.
o Use named numbers instead of constants when possible.
o Use named numbers to replace numeric literals whose type or context is
truly universal.
o Use constants for objects whose values cannot change after elaboration
(United Technologies 1987).
o Show relationships between symbolic values by defining them with
static expressions.
o Use linearly independent sets of literals.
o Use attributes like 'First and 'Last instead of literals wherever
possible.
example
3.141_592_653_589_793 -- literal
Max : constant Integer := 65_535; -- constant
Pi : constant := 3.141_592; -- named number
PI / 2 -- static expression
PI -- symbolic value
Declaring Pi as a named number allows it to be referenced symbolically in the
assignment statement below:
Area := Pi * Radius**2; -- if radius is known.
instead of
Area := 3.141_59 * Radius**2; -- Needs explanatory comment.
Also, ASCII.Bel is more expressive than Character'Val(8#007#).
Clarity of constant and named number declarations can be improved by using
other constant and named numbers. For example:
Bytes_Per_Page : constant := 512;
Pages_Per_Buffer : constant := 10;
Buffer_Size : constant := Pages_Per_Buffer * Bytes_Per_Page;
is more self-explanatory and easier to maintain than
Buffer_Size : constant := 5_120; -- ten pages
The following literals should be constants:
if New_Character = '$' then -- "constant" that may change
...
if Current_Column = 7 then -- "constant" that may change
rationale
Using identifiers instead of literals makes the purpose of expressions clear
reducing the need for comments. Constant declarations consisting of
expressions of numeric literals are safer since they do not need to be
computed by hand. They are also more enlightening than a single numeric
literal since there is more opportunity for embedding explanatory names.
Clarity of constant declarations can be improved further by using other
related constants in static expressions defining new constants. This is not
less efficient because static expressions of named numbers are computed at
compile time.
A constant has a type. A named number can only be a universal type: universal
integer or universal real. Strong typing is enforced for identifiers but not
literals. Named numbers allow compilers to generate more efficient code than
for constants and to perform more complete error checking at compile time. If
the literal contains a large number of digits (as Pi in the example above),
the use of an identifier reduces keystroke errors. If keystroke errors occur,
they are easier to locate either by inspection or at compile time.
Linear independence of literals means that the few literals that are used do
not depend on one another and that any relationship between constant or named
values is shown in the static expressions. Linear independence of literal
values gives the property that if one literal value changes, all of the named
numbers of values dependent on that literal are automatically changed.
note
There are some gray areas where the literal is actually more self-documenting
than a name. These are application-specific and generally occur with
universally familiar, unchangeable values such as the following relationship:
Fahrenheit := 32.0 + (9.0 * Celsius) / 5.0;
3.3 COMMENTS
Ada comments can be either beneficial or harmful to software maintainers. They
can be beneficial by explaining aspects of the code that are otherwise not
readily apparent. They can be harmful by containing inaccurate information and
by being too numerous and not visually distinct enough, which can cause them
to obscure the structure of the code.
Comments should be minimized. They should provide needed information that
cannot be expressed in the Ada language, emphasize the structure of code, and
draw attention to deliberate and necessary violations of the guidelines.
Comments are present either to draw attention to the real issue being
exemplified or to compensate for incompleteness in the example program.
Maintenance programmers need to know the causal interaction of noncontiguous
pieces of code to get a global, more or less complete sense of the program.
They typically acquire this kind of information from mental simulation of
parts of the code. Comments should be sufficient enough to support this
process (Soloway et al. 1986).
This section presents general guidelines about how to write good comments. It
then defines several different classes of comments with guidelines for the use
of each. The classes are: file headers, program unit specification headers,
program unit body headers, data comments, statement comments, and marker
comments.
3.3.1 General Comments
guideline
o Make the code as clear as possible to reduce the need for comments.
o Never repeat information in a comment which is readily available in
the code.
o Where a comment is required, make it concise and complete.
o Use proper grammar and spelling in comments.
o Make comments visually distinct from the code.
o Structure comments in headers so that information can be automatically
extracted by a tool.
rationale
The structure and function of well written code is clear without comments.
Obscure or badly structured code is hard to understand, maintain, or reuse
regardless of comments. Bad code should be improved, not explained. Reading
the code itself is the only way to be absolutely positive about what the code
does. Therefore, the code should be made as readable as possible.
Using comments to duplicate information in the code is a bad idea for several
reasons. First, it is unnecessary work that decreases productivity. Second, it
is very difficult to correctly maintain the duplication as the code is
modified. When changes are made to existing code, it is compiled and tested to
make sure that it is once again correct. However, there is no automatic
mechanism to make sure that the comments are correctly updated to reflect the
changes. Very often, the duplicate information in a comment becomes obsolete
at the first code change and remains so through the life of the software.
Third, when comments about an entire system are written from the limited point
of view of the author of a single subsystem, the comments are often incorrect
from the start.
Comments are necessary to reveal information difficult or impossible to obtain
from the code. Subsequent sections of this book contain examples of such
comments. Completely and concisely present the required information.
The purpose of comments is to help readers understand the code. Misspelled,
ungrammatical, ambiguous, or incomplete comments defeat this purpose. If a
comment is worth adding, it is worth adding correctly in order to increase its
usefulness.
Making comments visually distinct from the code, by indenting them, grouping
them together into headers, or highlighting them with dashed lines is useful
because it makes the code easier to read. Subsequent sections of this book
elaborate on this point.
automation note
The guideline about storing redundant information in comments applies only to
manually generated comments. There are tools which automatically maintain
information about the code (e.g., calling units, called units, cross-reference
information, revision histories, etc.), storing it in comments in the same
file as the code. Other tools read comments, but do not update them, using the
information from the comments to automatically generate detailed design
documents and other reports.
The use of such tools is encouraged, and may require that you structure your
header comments so they can be automatically extracted and/or updated. Beware
that tools which modify the comments in a file are only useful if they are
executed frequently enough. Automatically generated obsolete information is
even more dangerous than manually generated obsolete information, because it
is more trusted by the reader.
Revision histories are maintained much more accurately and completely by
configuration management tools. With no tool support, it is very common for an
engineer to make a change and forget to update the revision history. If your
configuration management tool is capable of maintaining revision histories as
comments in the source file, then take advantage of that capability,
regardless of any compromise you might have to make about the format or
location of the revision history. It is better to have a complete revision
history appended to the end of the file than to have a partial one formatted
nicely and embedded in the file header.
3.3.2 File Headers
guideline
o Put a file header on each source file.
o Place ownership, responsibility, and history information for the file
in the file header.
instantiation
- Put a copyright notice in the file header.
- Put the author's name and department in the file header.
- Put a revision history in the file header, including a summary of each
change, the date, and the name of the person making the change.
example
------------------------------------------------------------------------
-- Copyright (c) 1991, Software Productivity Consortium, Inc.
-- All rights reserved.
-- Author: J. Smith
-- Department:System Software Department
-- Revision History:
-- 7/9/91 J. Smith
-- - Added function Size_Of to support queries of node sizes.
-- - Fixed bug in Set_Size which caused overlap of large nodes.
-- 7/1/91 M. Jones
-- - Optimized clipping algorithm for speed.
-- 6/25/91 J. Smith
-- - Original version.
------------------------------------------------------------------------
rationale
Ownership information should be present in each file if you want to be sure to
protect your rights to the software. Furthermore, for high visibility, it
should be the very first thing in the file.
Responsibility and revision history information should be present in each file
for the sake of future maintainers, this is the header information most
trusted by maintainers because it accumulates. It does not evolve. There is no
need to ever go back and modify the author's name or the revision history of a
file. As the code evolves, the revision history should be updated to reflect
each change. At worst, it will be incomplete, it should rarely be wrong. Also,
the number and frequency of changes and the number of different people who
made the changes over the history of a unit can be good indicators of the
integrity of the implementation with respect to the design.
Information about how to find the original author should be included in the
file header, in addition to the author's name, to make it easier for
maintainers to find the author in case questions arise. However, detailed
information like phone numbers, mail stops, office numbers, and computer
account usernames are too volatile to be very useful. It is better to record
the department for which the author was working when the code was written.
This information is still useful if the author moves offices, changes
departments, or even leaves the company, because the department is likely to
retain responsibility for the original version of the code.
3.3.3 Program Unit Specification Header
guideline
o Put a header on the specification of each program unit.
o Place information required by the user of the program unit in the
specification header.
o Do not repeat information (except unit name) in the specification
header which is present in the specification.
o Explain what the unit does, not how or why it does it.
o Describe the complete interface to the program unit, including any
exceptions it can raise and any global effects it can have.
o Do not include information about how the unit fits into the enclosing
software system.
o Describe the performance (time and space) characteristics of the unit.
instantiation
- Put the name of the program unit in the header.
- Briefly explain the purpose of the program unit.
- For packages, describe the effects of the visible subprograms on each
other, and how they should be used together.
- List all exceptions which can be raised by the unit.
- List all global effects of the unit.
- List preconditions and postconditions of the unit.
- List hidden tasks activated by the unit.
- Do not list the names of parameters of a subprogram.
- Do not list the names of package subprograms just to list them.
- Do not list the names of all other units used by the unit.
- Do not list the names of all other units which use the unit.
example
------------------------------------------------------------------------
-- AUTOLAYOUT
-- Purpose:
-- This package computes positional information for nodes and arcs
-- of a directed graph. It encapsulates a layout algorithm which is
-- designed to minimize the number of crossing arcs and to emphasize
-- the primary direction of arc flow through the graph.
-- Effects:
-- - The expected usage is:
-- 1. Call Define for each node and arc to define the graph.
-- 2. Call Layout to assign positions to all nodes and arcs.
-- 3. Call Position_Of for each node and arc to determine the
-- assigned coordinate positions.
-- - Layout can be called multiple times, and recomputes the
-- positions of all currently defined nodes and arcs each time.
-- - Once a node or arc has been defined, it remains defined until
-- Clear is called to delete all nodes and arcs.
-- Performance:
-- This package has been optimized for time, in preference to space.
-- Layout times are on the order of N*log(N) where N is the number
-- of nodes, but memory space is used inefficiently.
------------------------------------------------------------------------
package Autolayout is
...
---------------------------------------------------------------------
-- Define
-- Purpose:
-- This procedure defines one node of the current graph.
-- Exceptions:
-- Node_Already_Defined
---------------------------------------------------------------------
procedure Define
(New_Node : in Node);
---------------------------------------------------------------------
-- Layout
-- Purpose:
-- This procedure assigns coordinate positions to all defined
-- nodes and arcs.
-- Exceptions:
-- None.
---------------------------------------------------------------------
procedure Layout;
---------------------------------------------------------------------
-- Position_Of
-- Purpose:
-- This function returns the coordinate position of the
-- specified node. The default position (0,0) is returned if no
-- position has been assigned yet.
-- Exceptions:
-- Node_Not_Defined
---------------------------------------------------------------------
function Position_Of (Current : in Node)
return Position;
...
end Autolayout;
rationale
The purpose of a header comment on the specification of a program unit is to
help the user understand how to use the program unit. From reading the program
unit specification and header, a user should know everything necessary to use
the unit. It should not be necessary to read the body of the program unit.
Therefore, there should be a header comment on each program unit
specification, and each header should contain all usage information not
expressed in the specification itself. Such information includes the units'
effect on each other and on shared resources, exceptions raised, and
time/space characteristics. None of this information can be determined from
the Ada specification of the program unit.
When you duplicate information in the header that can be readily obtained from
the specification, the information tends to become incorrect during
maintenance. For example, do not make a point of listing all parameter names,
modes, or types when describing a procedure. This information is already
available from the procedure specification. Similarly, do not list all
subprograms of a package in the header unless this is necessary to make some
important statement about the subprograms.
Do not include information in the header which the user of the program unit
doesn't need. In particular, do not include information about how a program
unit performs its function or why a particular algorithm was used. This
information should be hidden in the body of the program unit to preserve the
abstraction defined by the unit. If the user knows such details and makes
decisions based on that information, the code may suffer when that information
is later changed.
When describing the purpose of the unit, avoid referring to other parts of the
enclosing software system. It is better to say "this unit does ..." than to
say "this unit is called by Xyz to do ...." The unit should be written in such
a way that it does not know or care which unit is calling it. This makes the
unit much more general purpose and reusable. In addition, information about
other units is likely to become obsolete and incorrect during maintenance.
Include information about the performance (time and space) characteristics of
the unit. Much of this information is not present in the Ada specification,
but it is required by the user. To integrate the unit into a system, the user
needs to understand the resource usage (CPU, memory, etc.) of the unit. It is
especially important to note when a subprogram call causes activation of a
task hidden in a package body, the task may continue to consume resources
after the subroutine ends.
exception
Where a group of program units are closely related or simple to understand, it
is acceptable to use a single header for the entire group of program units.
For example, it makes sense to use a single header to describe the behavior of
Max and Min functions; Sin, Cos, and Tan functions; or a group of functions to
query related attributes of an object encapsulated in a package. This is
especially true when each function in the set is capable of raising the same
exceptions.
3.3.4 Program Unit Body Header
guideline
o Place information required by the maintainer of the program unit in
the body header.
o Explain how and why the unit performs its function, not what the unit
does.
o Do not repeat information (except unit name) in the header that is
readily apparent from reading the code.
o Do not repeat information (except unit name) in the body header that
is available in the specification header.
instantiation
- Put the name of the program unit in the header.
- Record portability issues in the header.
- Summarize complex algorithms in the header.
- Record reasons for significant or controversial implementation
decisions.
- Record discarded implementation alternatives, along with the reason
for discarding them.
- Record anticipated changes in the header, especially if some work has
already been done to the code to make the changes easy to accomplish.
example
------------------------------------------------------------------------
-- Autolayout
-- Implementation Notes:
-- - This package uses a heuristic algorithm to minimize the number
-- of arc crossings. It does not always achieve the true minimum
-- number which could theoretically be reached. However it does a
-- nearly perfect job in relatively little time. For details about
-- the algorithm, see ...
-- Portability Issues:
-- - The native math package Math_Lib is used for computations of
-- coordinate positions.
-- - 32-bit integers are required.
-- - No operating system specific routines are called.
-- Anticipated Changes:
-- - Coordinate_Type below could be changed from integer to float
-- with little effort. Care has been taken to not depend on the
-- specific characteristics of integer arithmetic.
------------------------------------------------------------------------
package body Autolayout is
...
---------------------------------------------------------------------
-- Define
-- Implementation Notes:
-- - This routine stores a node in the general purpose Graph data
-- structure, not the Fast_Graph structure because ...
---------------------------------------------------------------------
procedure Define
(New_Node : in Node) is
begin
...
end Define;
---------------------------------------------------------------------
-- Layout
-- Implementation Notes:
-- - This routine copies the Graph data structure (optimized for
-- fast random access) into the Fast_Graph data structure
-- (optimized for fast sequential iteration), then performs the
-- layout, and copies the data back to the Graph structure. This
-- technique was introduced as an optimization when the algorithm
-- was found to be too slow, and it produced an order of
-- magnitude improvement.
---------------------------------------------------------------------
procedure Layout is
begin
...
end Layout;
---------------------------------------------------------------------
-- Position_Of
---------------------------------------------------------------------
function Position_Of (Current : in Node)
return Position is
begin
...
end Position_Of;
...
end Autolayout;
rationale
The purpose of a header comment on the body of a program unit is to help the
maintainer of the program unit to understand the implementation of the unit,
including tradeoffs among different techniques. Be sure to document all
decisions made during implementation to prevent the maintainer from making the
same mistakes you made. One of the most valuable comments to a maintainer is a
clear description of why a change being considered will not work.
The header is also a good place to record portability concerns. The maintainer
may have to port the software to a different environment and will benefit from
a list of nonportable features. Furthermore, the act of collecting and
recording portability issues focuses attention on these issues and may result
in more portable code from the start.
Summarize complex algorithms in the header if the code is difficult to read or
understand without such a summary, but do not merely paraphrase the code. Such
duplication is unnecessary and hard to maintain. Similarly, do not repeat the
information from the header of the program unit specification.
note
It is often the case that a program unit is self-explanatory enough that it
requires no body header to explain how it is implemented or why. In such a
case, omit the header entirely, as in the case with Position_Of above. Be
sure, however, that the header you omit truly contains no information. For
example, consider the difference between the two header sections:
-- Implementation Notes: None.
and
-- NonPortable Features: None.
The first is a message from the author to the maintainer saying "I can't think
of anything else to tell you," while the second may mean "I guarantee that this
unit is entirely portable."
3.3.5 Data Comments
guideline
o Comment on all data types, objects, and exceptions unless their names
are self-explanatory.
o Include information on the semantic structure of complex pointer-based
data structures.
o Include information about relationships that are maintained between
data objects.
o Do not include comments that merely repeat the information in the name.
example
Objects can be grouped by purpose and commented as:
...
---------------------------------------------------------------------
-- Current position of the cursor in the currently selected text
-- buffer, and the most recent position explicitly marked by the
-- user.
-- Note: It is necessary to maintain both current and desired
-- column positions because the cursor cannot always be
-- displayed in the desired position when moving between
-- lines of different lengths.
---------------------------------------------------------------------
Desired_Column : Column_Counter;
Current_Column : Column_Counter;
Current_Row : Row_Counter;
Marked_Column : Column_Counter;
Marked_Row : Row_Counter;
The conditions under which an exception is raised should be commented:
---------------------------------------------------------------------
-- Exceptions
---------------------------------------------------------------------
Node_Already_Defined : exception; -- Raised when an attempt is made
--| to define a node with an
--| identifier which already
--| defines a node.
Node_Not_Defined : exception; -- Raised when a reference is
--| made to a node which has
--| not been defined.
Here is a more complex example, involving multiple record and access types
which are used to form a complex data structure:
---------------------------------------------------------------------
-- These data structures are used to store the graph during the
-- layout process. The overall organization is a sorted list of
-- "ranks," each containing a sorted list of nodes, each containing
-- a list of incoming arcs and a list of outgoing arcs.
-- The lists are doubly linked to support forward and backward
-- passes for sorting. Arc lists do not need to be doubly linked
-- because order of arcs is irrelevant.
-- The nodes and arcs are doubly linked to each other to support
-- efficient lookup of all arcs to/from a node, as well as efficient
-- lookup of the source/target node of an arc.
---------------------------------------------------------------------
type Arc;
type Arc_Pointer is access Arc;
type Node;
type Node_Pointer is access Node;
type Node is
record
Id : Node_Pointer;-- Unique node ID supplied by the user.
Arc_In : Arc_Pointer;
Arc_Out : Arc_Pointer;
Next : Node_Pointer;
Previous : Node_Pointer;
end record;
type Arc is
record
ID : Arc_ID; -- Unique arc ID supplied by the user.
Source : Node_Pointer;
Target : Node_Pointer;
Next : Arc_Pointer;
end record;
type Rank;
type Rank_Pointer is access Rank;
type Rank is
record
Number : Level_ID; -- Computed ordinal number of the rank.
First_Node : Node_Pointer;
Last_Node : Node_Pointer;
Next : Rank_Pointer;
Previous : Rank_Pointer;
end record;
First_Rank : Rank_Pointer;
Last_Rank : Rank_Pointer;
rationale
It is very useful to add comments explaining the purpose, structure, and
semantics of the data structures. Many maintainers look at the data structures
first when trying to understand the implementation of a unit. Understanding
the data which can be stored, along with the relationships between the
different data items, and the flow of data through the unit is an important
first step in understanding the details of the unit.
In the first example above, the names Current_Column and Current_Row are
relatively self-explanatory. The name Desired_Column is also well chosen, but
it leaves the reader wondering what the relationship is between the current
column and the desired column. The comment explains the reason for having both.
Another advantage of commenting on the data declarations is that the single
set of comments on a declaration can replace multiple sets of comments that
might otherwise be needed at various places in the code where the data is
manipulated. In the first example above, the comment briefly expands on the
meaning of "current" and "marked." It states that the "current" position is
the location of the cursor, the "current" position is in the current buffer,
and the "marked" position was marked by the user. This comment, along with the
mnemonic names of the variables, greatly reduces the need for comments at
individual statements throughout the code.
It is important to document the full meaning of exceptions and under what
conditions they can be raised, as shown in the second example above,
especially when the exceptions are declared in a package specification. The
reader has no other way to find out the exact meaning of the exception
(without reading the code in the package body).
Grouping all the exceptions together, as shown in the second example, can
provide the reader with the effect of a "glossary" of special conditions. This
is useful when many different subprograms in the package can raise the same
exceptions. For a package in which each exception can be raised by only one
subprogram, it may be better to group related subprograms and exceptions
together.
When commenting exceptions, it is better to describe the exception's meaning
in general terms than to list all the subprograms that can cause the exception
to be raised; such a list is harder to maintain. When a new routine is added,
it is likely that these lists will not be updated. Also, this information is
already present in the comments describing the subprograms, where all
exceptions that can be raised by the subprogram should be listed. Lists of
exceptions by subprogram are more useful and easier to maintain than lists of
subprograms by exception.
In the third example, the names of the record fields are short and mnemonic,
but they are not completely self-explanatory. This is often the case with
complex data structures involving access types. There is no way to choose the
record and field names so that they completely explain the overall
organization of the records and pointers into a nested set of sorted lists.
The comments shown are useful in this case. Without them, the reader would not
know which lists are sorted, which lists are doubly linked, or why. The
comments express the intent of the author with respect to this complex data
structure. The maintainer still has to read the code if he wants to be sure
that the double links are all properly maintained. Keeping this in mind when
reading the code makes it much easier for him to find a bug where one pointer
is updated and the opposite one is not.
3.3.6 Statement Comments
guideline
o Minimize comments embedded among statements.
o Use comments only to explain parts of the code that are not obvious.
o Comment intentional omissions from the code.
o Do not use comments to paraphrase the code.
o Do not use comments to explain remote pieces of code, such as
subprograms called by the current unit.
o Where comments are necessary, make them visually distinct from the
code.
example
The following is an example of very poorly commented code:
...
-- Loop through all the strings in the array Strings, converting
-- them to integers by calling Convert_To_Integer on each one,
-- accumulating the sum of all the values in Sum, and counting them
-- in Count. Then divide Sum by Count to get the average and store
-- it in Average. Also, record the maximum number in the global
-- variable Max_Number.
for I in Strings'Range loop
-- Convert each string to an integer value by looping through
-- the characters which are digits, until a nondigit is found,
-- taking the ordinal value of each, subtracting the ordinal value
-- of '0', and multiplying by 10 if another digit follows. Store
-- the result in Number.
Number := Convert_To_Integer(Strings(I));
-- Accumulate the sum of the numbers in Total.
Sum := Sum + Number;
-- Count the numbers.
Count := Count + 1;
-- Decide whether this number is more than the current maximum.
if Number > Max_Number then
-- Update the global variable Max_Number.
Max_Number := Number;
end if;
end loop;
-- Compute the average.
Average := Sum / Count;
The following is improved by not repeating things in the comments which are
obvious from the code, not describing the details of what goes in inside of
Convert_To_Integer, deleting an erroneous comment (the one on the statement
which accumulates the sum), and making the few remaining comments more
visually distinct from the code.
Sum_Integers_Converted_From_Strings:
for I in Strings'Range loop
Number := Convert_To_Integer(Strings(I));
Sum := Sum + Number;
Count := Count + 1;
-- The global Max_Number is computed here for efficiency.
if Number > Max_Number then
Max_Number := Number;
end if;
end loop Sum_Integers_Converted_From_Strings;
Average := Sum / Count;
rationale
The improvements shown in the example are not improvements merely by reducing
the total number of comments; they are improvements by reducing the number of
useless comments.
Comments that paraphrase or explain obvious aspects of the code have no value.
They are a waste of effort for the author to write and the maintainer to
update. Therefore, they often end up becoming incorrect. Such comments also
clutter the code, hiding the few important comments.
Comments describing what goes on inside of another unit violate the principle
of information hiding. The details about Convert_To_Integer (deleted above)
are irrelevant to the calling unit, and they are better left hidden in case
the algorithm ever changes. Examples explaining what goes on elsewhere in the
code are very difficult to maintain and almost always become incorrect at the
first code modification.
The advantage of making comments visually distinct from the code is that it
makes the code easier to scan, and the few important comments stand out
better. Highlighting unusual or special code features indicates that they are
intentional. This assists maintainers by focusing attention on code sections
that are likely to cause problems during maintenance or when porting the
program to another implementation.
Comments should be used to document code that is nonportable,
implementation-dependent, environment-dependent, or tricky in any way. They
notify the reader that something unusual was put there for a reason. A
beneficial comment would be one explaining a work-around for a compiler bug. If
you use a lower level (not "ideal" in the software engineering sense) solution,
comment on it. Information included in the comments should state why you used
that particular construct. Also include documentation on the failed attempts,
e.g., using a higher level structure. This type of comment is useful to
maintainers for historical purposes. You show the reader that a significant
amount of thought went into the choice of a construct.
Finally, comments should be used to explain what is not present in the code as
well as what is present. If you make a conscious decision to not perform some
action, like deallocating a data structure with which you appear to be
finished, be sure to add a comment explaining why not. Otherwise, a maintainer
may notice the apparent omission and "correct" it later, thus introducing an
error.
note
Further improvements can be made on the above example by declaring the
variables Count and Sum in a local block so that their scope is limited and
their initializations occur near their usage, e.g., by naming the block
Compute_Average or by moving the code into a function called Average_Of. The
computation of Max_Number can also be separated from the computation of
Average. However, those changes are the subject of other guidelines; this
example is only intends to illustrate the proper use of comments.
3.3.7 Marker Comments
guideline
o Use pagination markers to mark program unit boundaries (Guideline ).
o Repeat the unit name in a comment to mark the begin of a package body,
subprogram body, task body, or block if the begin is preceded by declarations.
o For long or heavily nested if and case statements, mark the end of the
statement with a comment summarizing the condition governing the statement.
o For long or heavily nested if statements, mark the else part with a
comment summarizing the conditions governing this portion of the statement.
example
if A_Found then
...
elsif B_Found then
...
else -- A and B were both not found
...
if Count = Max then
...
end if;
...
end if; -- A_Found
------------------------------------------------------------------------
package body Abstract_Strings is
...
---------------------------------------------------------------------
procedure Catenate (...) is
begin
...
end Catenate;
---------------------------------------------------------------------
...
begin -- Abstract_Strings
...
end Abstract_Strings;
------------------------------------------------------------------------
rationale
Marker comments emphasize the structure of code and make it easier to scan.
They can be lines that separate sections of code or descriptive tags for a
construct. They help the reader resolve questions about the current position in
the code. This is more important for large units than for small ones. A short
marker comment fits on the same line as the reserved word with which it is
associated. Thus, it adds information without clutter.
The if, elsif, else, and end if of an if statement are often separated by long
sequences of statements, sometimes involving other if statements. As shown in
the first example, marker comments emphasize the association of the keywords
of the same statement over a great visual distance. Marker comments are not
necessary with the block statement and loop statement because the syntax of
these statements allows them to be named with the name repeated at the end.
Using these names is better than using marker comments because the compiler
verifies that the names at the beginning and end match.
The sequence of statements of a package body is often very far from the first
line of the package. Many subprogram bodies, each containing many begin lines,
may occur first. As shown in the second example, the marker comment emphasizes
the association of the begin with the package.
note
Repeating names and noting conditional expressions clutters the code if
overdone. It is visual distance, especially page breaks, that makes marker
comments beneficial.
3.4 USING TYPES
Strong typing promotes reliability in software. The type definition of an
object defines all legal values and operations and allows the compiler to
check for and identify potential errors during compilation. In addition, the
rules of type allow the compiler to generate code to check for violations of
type constraints at execution time. Using these Ada compiler's features
facilitates earlier and more complete error detection than that which is
available with less strongly typed languages.
3.4.1 Declaring Types
guideline
o Limit the range of scalar types as much as possible.
o Seek information about possible values from the application.
o Do not overload any of the type names in package Standard.
o Use subtype declarations to improve program readability (Booch 1987).
o Use derived types and subtypes in concert (see Guideline ).
example
subtype Card_Image is String (1 .. 80);
Input_Line : Card_Image := (others => ' ');
-- restricted integer type:
type Day_Of_Leap_Year is range 1 .. 366;
subtype Day_Of_Non_Leap_Year is Day_Of_Leap_Year range 1 .. 365;
By the following declaration, the programmer means, "I haven't the foggiest
idea how many," but the actual range will show up buried in the code or as a
system parameter:
Employee_Count : Integer;
rationale
Eliminating meaningless values from the legal range improves the compiler's
ability to detect errors when an object is set to an invalid value. This also
improves program readability. In addition, it forces you to carefully think
about each use of objects declared to be of the subtype.
Different implementations provide different sets of values for most of the
predefined types. A reader cannot determine the intended range from the
predefined names. This situation is aggravated when the predefined names are
overloaded.
The names of an object and its subtype can clarify their intended use and
document low-level design decisions. The example above documents a design
decision to restrict the software to devices whose physical parameters are
derived from the characteristics of punch cards. This information is easy to
find for any later changes, thus enhancing program maintainability.
Section 8.5 of the Ada Language Reference Manual says that declaring a subtype
without a constraint is one method for renaming a type.
Types can have highly constrained sets of values without eliminating useful
values. Usage as described in Guideline eliminates many flag variables and
type conversions within executable statements. This renders the program more
readable while allowing the compiler to enforce strong typing constraints.
note
Subtype declarations do not define new types, only constraints for existing
types.
Recognize that any deviation from this guideline detracts from the advantages
of the strong typing facilities of the Ada language.
3.4.2 Enumeration Types
guideline
o Use enumeration types instead of numeric codes.
o Use representation clauses to match requirements of external devices.
example
Use
type Color is (Blue, Red, Green, Yellow);
rather than
Blue : constant := 1;
Red : constant := 2;
Green : constant := 3;
Yellow : constant := 4;
and add the following if necessary.
for Color use (Blue => 1,
Red => 2,
Green => 3,
Yellow => 4);
rationale
Enumerations are more robust than numeric codes; they leave less potential for
errors resulting from incorrect interpretation and from additions to and
deletions from the set of values during maintenance. Numeric codes are
holdovers from languages that have no user-defined types.
In addition, Ada provides a number of attributes ('Pos, 'Val, 'Succ, 'Pred,
'Image, and 'Value) for enumeration types which, when used, are more reliable
than user-written operations on encodings.
A numeric code may at first seem appropriate to match external values.
Instead, these situations call for a representation clause on the enumeration
type. The representation clause documents the "encoding." If the program is
properly structured to isolate and encapsulate hardware dependencies (see
Guideline ), the numeric code ends up in an interface package where it can be
easily found and replaced should the requirements change.
3.5 SUMMARY
spelling
o Use underscores to separate words in a compound name.
o Represent numbers in a consistent fashion.
o Represent literals in a radix appropriate to the problem.
o Use underscores to separate digits the same way commas or periods (or
spaces for nondecimal bases) would be used in handwritten text.
o When using scientific notation, make the E consistently either upper
or lower case.
o In an alternate base, represent the alphabetic characters in either
all upper case or all lower case.
o Make reserved words and other elements of the program visually
distinct from each other.
o Do not use an abbreviation of a long word as an identifier where a
shorter synonym exists.
o Use a consistent abbreviation strategy.
o Do not use ambiguous abbreviations.
o An abbreviation must save many characters over the full word to be
justified.
o Use abbreviations that are well-accepted in the application domain.
o Maintain a list of accepted abbreviations and use only abbreviations
on that list.
naming conventions
o Choose names that are as self-documenting as possible.
o Use a short synonym instead of an abbreviation (see Guideline 3.1.4).
o Use names given by the application but not obscure jargon.
o Use singular, general nouns as (sub)type identifiers.
o Choose identifiers that describe one of the (sub)type's values.
o Do not use identifier constructions (e.g., suffixes) that are unique
to (sub)type identifiers.
o Do not use the type names from predefined packages.
o Use predicate clauses or adjectives for boolean objects.
o Use singular, specific nouns as object identifiers.
o Choose identifiers that describe the object's value during execution.
o Use singular, general nouns as identifiers for record components.
o Use action verbs for procedures and entries.
o Use predicate-clauses for boolean functions.
o Use nouns for nonboolean functions.
o Give packages names that imply higher levels of organization than
subprograms. Generally, these are noun phrases that describe the abstraction
provided.
o Give tasks names that imply an active entity.
o Name generic subprograms as if they were nongeneric subprograms.
o Name generic packages as if they were nongeneric packages.
o Make the generic names more general than the instantiated names.
o Use symbolic values instead of literals wherever possible.
o Use constants instead of variables for constant values.
o Use named numbers instead of constants when possible.
o Use named numbers to replace numeric literals whose type or context is
truly universal.
o Use constants for objects whose values cannot change after elaboration
(United Technologies 1987).
o Show relationships between symbolic values by defining them with
static expressions.
o Use linearly independent sets of literals.
o Use attributes like 'First and 'Last instead of literals wherever
possible.
comments
o Make the code as clear as possible to reduce the need for comments.
o Never repeat information in a comment which is readily available in
the code.
o Where a comment is required, make it concise and complete.
o Use proper grammar and spelling in comments.
o Make comments visually distinct from the code.
o Structure comments in headers so that information can be automatically
extracted by a tool.
o Put a file header on each source file.
o Place ownership, responsibility, and history information for the file
in the file header.
o Put a header on the specification of each program unit.
o Place information required by the user of the program unit in the
specification header.
o Do not repeat information (except unit name) in the specification
header which is present in the specification.
o Explain what the unit does, not how or why it does it.
o Describe the complete interface to the program unit, including any
exceptions it can raise and any global effects it can have.
o Do not include information about how the unit fits into the enclosing
software system.
o Describe the performance (time and space) characteristics of the unit.
o Place information required by the maintainer of the program unit in
the body header.
o Explain how and why the unit performs its function, not what the unit
does.
o Do not repeat information (except unit name) in the header that is
readily apparent from reading the code.
o Do not repeat information (except unit name) in the body header that
is available in the specification header.
o Comment on all data types, objects, and exceptions unless their names
are self-explanatory.
o Include information on the semantic structure of complex pointer-based
data structures.
o Include information about relationships that are maintained between
data objects.
o Do not include comments that merely repeat the information in the name.
o Minimize comments embedded among statements.
o Use comments only to explain parts of the code that are not obvious.
o Comment intentional omissions from the code.
o Do not use comments to paraphrase the code.
o Do not use comments to explain remote pieces of code, such as
subprograms called by the current unit.
o Where comments are necessary, make them visually distinct from the
code.
o Use pagination markers to mark program unit boundaries (Guideline ).
o Repeat the unit name in a comment to mark the begin of a package body,
subprogram body, task body, or block if the begin is preceded by declarations.
o For long or heavily nested if and case statements, mark the end of the
statement with a comment summarizing the condition governing the statement.
o For long or heavily nested if statements, mark the else part with a
comment summarizing the conditions governing this portion of the statement.
using types
o Limit the range of scalar types as much as possible.
o Seek information about possible values from the application.
o Do not overload any of the type names in package Standard.
o Use subtype declarations to improve program readability (Booch 1987).
o Use derived types and subtypes in concert (see Guideline ).
o Use enumeration types instead of numeric codes.
o Use representation clauses to match requirements of external devices.
CHAPTER 4
Program Structure
Proper structure improves program clarity. This is analogous to readability on
lower levels and facilitates the use of the readability guidelines (Chapter
3). The various program structuring facilities provided by Ada were designed
to enhance overall clarity of design. These guidelines show how to use these
facilities for their intended purposes.
Abstraction and encapsulation are supported by the package concept and by
private types. Related data and subprograms can be grouped together and seen
by a higher level as a single entity. Information hiding is enforced via
strong typing and by the separation of package and subprogram specifications
from their bodies. Additional Ada language elements that impact program
structure are exceptions and tasks.
4.1 HIGH-LEVEL STRUCTURE
Well-structured programs are easily understood, enhanced, and maintained.
Poorly structured programs are frequently restructured during maintenance just
to make the job easier. Many of the guidelines listed below are often given as
general program design guidelines.
4.1.1 Separate Compilation Capabilities
guideline
o Place the specification of each library unit package in a separate
file from its body.
o Create an explicit specification, in a separate file, for each library
unit subprogram.
o Use subunits for the bodies of large units which are nested in other
units.
o Place each subunit in a separate file.
o Use a consistent file naming convention.
example
The file names below illustrate one possible file organization and associated
consistent naming convention. The library unit name is used for the body. A
trailing underscore indicates the specification, and any files containing
subunits use names constructed by separating the body name from the subunit
name with two underscores.
text_io_.ada -- the specification
text_io.ada -- the body
text_io__integer_io.ada -- a subunit
text_io__fixed_io.ada -- a subunit
text_io__float_io.ada -- a subunit
text_io__enumeration_io.ada -- a subunit
rationale
The main reason for the emphasis on separate files in this guideline is to
minimize the amount of recompilation required after each change. Typically,
during software development, bodies of units are updated far more often than
specifications. If the body and specification reside in the same file, then
the specification will be compiled each time the body is compiled, even though
the specification has not changed. Because the specification defines the
interface between the unit and all of its users, this recompilation of the
specification typically makes recompilation of all users necessary, in order
to verify compliance with the specification. If the specifications and bodies
of the users also reside together, then any users of these units will also
have to be recompiled, and so on. The ripple effect can force a huge number of
compilations which could have been avoided, severely slowing the development
and test phase of a project. This is why we suggest placing specifications of
all library units (nonnested units) in separate files from their bodies.
For the same reason, use subunits for large nested bodies, and put each
subunit in its own file. This makes it possible to modify the body of the one
nested unit without having to recompile any of the other units in the body.
This is recommended for large units because changes are more likely to occur
in large units than in small ones.
An additional benefit of using multiple separate files is that it allows
different implementers to modify different parts of the system at the same
time with conventional editors which do not allow multiple concurrent updates
to a single file.
Finally, keeping bodies and specifications separate makes it possible to have
multiple bodies for the same specification, or multiple specifications for the
same body. Although Ada requires that there be exactly one specification per
body in a system at any given time, it can still be useful to maintain
multiple bodies or multiple specifications for use in different builds of a
system. For example, a single specification may have multiple bodies, each of
which implements the same functionality with a different tradeoff of time
versus space efficiency. Or, for machine-dependent code, there may be one body
for each target machine. Maintaining multiple package specifications can also
be useful during development and test. You may develop one specification for
delivery to your customer and another for unit testing. The first one would
export only those subprograms intended to be called from outside of the
package during normal operation of the system. The second one would export all
subprograms of the package so that each of them could be independently tested.
A consistent file naming convention is recommended to make it easier to manage
the large number of files which may result from following this guideline.
4.1.2 Subprograms
guideline
o Use subprograms to enhance abstraction.
o Restrict each subprogram to the performance of a single action.
example
Your program is required to output text to many types of devices. Because the
devices would accept a variety of character sets, the proper way to do this is
to write a subprogram to convert to the required character set. This way, the
output subprogram has one purpose and the conversions are described elsewhere.
...
----------------------------------------------------------------------
procedure Dispatch_To_Device
(Output : in Text;
Device : in Device_Name;
Status : out Error_Codes) is
Upper_Case_Output : Text (1 .. Output'Length);
...
begin -- Dispatch_To_Device
...
case Device.Character_Set is
when Limited_ASCII =>
Convert_To_Upper_Case(Original => Output,
Upper_Case => Upper_Case_Output);
...
when Extended_ASCII =>
...
when EBCDIC =>
...
end case; -- Device_Type.Character_Set
...
end Dispatch_To_Device;
----------------------------------------------------------------------
rationale
Subprograms are an extremely effective and well-understood abstraction
technique. Subprograms increase program readability by hiding the details of a
particular activity. It is not necessary that a subprogram be called more than
once to justify its existence.
note
Dealing with the overhead of subroutine calls is discussed in Guideline .
4.1.3 Functions
guideline
o Use a function when the subprogram's primary purpose is to provide a
single value.
o Minimize the side effect of a function.
example
Although reading a character from a file will change what character is read
next, this is accepted as a minor side effect compared to the primary purpose
of the following function:
function Next_Character return Character is separate;
However, the use of a function like this should could lead to a subtle
problem. Any time the order of evaluation is undefined, the order of the
values returned by the function will effectively be undefined. In this
example, the order of the characters placed in Word and the order that the
following two characters are given to the Suffix parameters is unknown. No
implementation of the Next_Character function can guarantee which character
will go where:
Word : constant String := String'(1 .. 5 => Next_Character);
begin -- Start_Parsing
Parse(Keyword => Word,
Suffix1 => Next_Character,
Suffix2 => Next_Character);
end Start_Parsing;
Of course, if the order is unimportant (as in a random number generator), then
the order of evaluation is unimportant.
rationale
A side effect is a change to any variable that is not local to the subprogram.
This includes changes to variables by other subprograms and entries during
calls from the function if the changes persist after the function returns.
Side effects are discouraged because they are difficult to understand and
maintain. Additionally, the Ada language does not define the order in which
functions are evaluated when they occur in expressions or as actual parameters
to subprograms. Therefore, a program which depends on the order in which side
effects of functions occur is erroneous. Avoid using side effects anywhere.
4.1.4 Packages
guideline
o Use packages for information hiding.
o Use packages with private types for abstract data types.
o Use packages to model abstract entities appropriate to the problem
domain.
o Use packages to group together related type and object declarations
(e.g., common declarations for two or more library units).
o Use packages to group together related program units for configuration
control or visibility reasons (NASA 1987).
o Encapsulate machine dependencies in packages. Place a software
interface to a particular device in a package to facilitate a change to a
different device.
o Place low-level implementation decisions or interfaces in subprograms
within packages.
o Use packages and subprograms to encapsulate and hide program details
that may change (Nissen and Wallis 1984).
example
A package called Backing_Storage_Interface could contain type and subprogram
declarations to support a generalized view of an external memory system (such
as a disk or drum). Its internals may, in turn, depend on other packages more
specific to the hardware or operating system.
rationale
Packages are the principal structuring facility in Ada. They are intended to
be used as direct support for abstraction, information hiding, and
modularization. For example, they are useful for encapsulating machine
dependencies as an aid to portability. A single specification can have
multiple bodies isolating implementation-specific information so other parts
of the code do not need to change.
Encapsulating areas of potential change helps to minimize the effort required
to implement that change by preventing unnecessary dependencies among
unrelated parts of the system.
note
The most prevalent objection to this guideline usually involves performance
penalties. See Guideline for a discussion about subprogram overhead.
4.1.5 Cohesion
guideline
o Make each package serve a single purpose.
o Use packages to group related data, types, and subprograms.
o Avoid collections of unrelated objects and subprograms (NASA 1987 and
Nissen and Wallis 1984).
example
As a bad example, a package named Project_Definitions is obviously a "catch
all" for a particular project and is likely to be a jumbled mess. It probably
has this form to permit project members to incorporate a single with clause
into their software.
Better examples are packages called Display_Format_Definitions, containing all
the types and constants needed by some specific display in a specific format,
and Cartridge_Tape_Handler, containing all the types, constants, and
subprograms which provide an interface to a special purpose device.
rationale
See also Guideline on Heterogeneous Data.
The degree to which the entities in a package are related has a direct impact
on the ease of understanding packages and programs made up of packages. There
are different criteria for grouping, and some criteria are less effective than
others. Grouping the class of data or activity (e.g., initialization modules)
or grouping data or activities based on their timing characteristics is less
effective than grouping based on function or need to communicate through data
(Charette 1986 paraphrased).
note
Traditional subroutine libraries often group functionally unrelated
subroutines. Even such libraries should be broken into a collection of
packages each containing a logically cohesive set of subprograms.
4.1.6 Data Coupling
guideline
o Avoid declaring variables in package specifications.
example
This is part of a compiler. Both the package handling error messages and the
package containing the code generator need to know the current line number.
Rather than storing this in a shared variable of type Natural, the information
is stored in a package that hides the details of how such information is
represented, and makes it available with access routines.
-------------------------------------------------------------------------
package Compilation_Status is
type Line_Range is range 1 .. 2_500_000;
function Source_Line_Number return Line_Range;
end Compilation_Status;
-------------------------------------------------------------------------
with Compilation_Status;
package Error_Message_Processing is
-- Handle compile-time diagnostic.
end Error_Message_Processing;
-------------------------------------------------------------------------
with Compilation_Status;
package Code_Generation is
-- Operations for code generation.
end Code_Generation;
-------------------------------------------------------------------------
rationale
Strongly coupled program units can be difficult to debug and very difficult to
maintain. By protecting shared data with access functions, the coupling is
lessened. This prevents dependence on the data structure and access to the
data can be controlled.
note
The most prevalent objection to this guideline usually involves performance
penalties. When a variable is moved to the package body, subprograms to
access the variable must be provided and the overhead involved during each
call to those subprograms is introduced. See Guideline for a discussion about
subprogram overhead.
4.1.7 Tasks
guideline
o Use tasks to model abstract, asynchronous entities within the problem
domain.
o Use tasks to control or synchronize access to tasks or other
asynchronous entities (e.g., asynchronous I/O, peripheral devices, interrupts).
o Use tasks to define concurrent algorithms for multiprocessor
architectures.
o Use tasks to perform concurrent, cyclic, or prioritized activities
(NASA 1987).
rationale
The rationale for this guideline is given under Guideline . Chapter 6
discusses tasking in more detail.
4.2 VISIBILITY
Ada's ability to enforce information hiding and separation of concerns through
its visibility controlling features is one of the most important advantages of
the language, particularly when "pieces of a large system are being developed
separately." Subverting these features, for example by excessive reliance on
the use clause, is wasteful and dangerous. See also Guideline .
4.2.1 Minimization of Interfaces
guideline
o Put only what is needed for the use of a package into its
specification.
o Minimize the number of declarations in package specifications.
o Do not include extra operations simply because they are easy to build.
o Minimize the context (with) clauses in a package specification.
o Reconsider subprograms which seem to require large numbers of
parameters.
o Do not manipulate global data within a subprogram or package merely to
limit the number of parameters.
o Avoid unnecessary visibility; hide the implementation details of a
program unit from its users.
example
-------------------------------------------------------------------------
package Telephone_Book is
type Listing is limited private;
procedure Set_Name (New_Name : in String;
Current : in out Listing);
procedure Insert (Additional : in Listing);
procedure Delete (Obsolete : in Listing);
private
type Information;
type Listing is access Information;
end Telephone_Book;
-------------------------------------------------------------------------
package body Telephone_Book is
-- Full details of record for a listing
type Information is
record
...
Next : Listing;
end record;
First : Listing;
procedure Set_Name (New_Name : in String;
Current : in out Listing) is separate;
procedure Insert (Additional : in Listing) is separate;
procedure Delete (Obsolete : in Listing) is separate;
end Telephone_Book;
-------------------------------------------------------------------------
rationale
For each entity in the specification, give careful consideration to whether it
could be moved to the body. The fewer the extraneous details, the more
understandable the program, package, or subprogram. It is important to
maintainers to know exactly what a package interface is so that they can
understand the effects of changes. Interfaces to a subprogram extend beyond
the parameters. Any modification of global data from within a package or
subprogram is an undocumented interface to the "outside" as well.
Pushing as many as possible of the context dependencies into the body makes
the reader's job easier, localizes the recompilation required when library
units change, and helps prevent a ripple effect during modifications. See also
Guideline .
Subprograms with large numbers of parameters often indicate poor design
decisions (e.g., the functional boundaries of the subprogram are
inappropriate, or parameters are structured poorly). Conversely, subprograms
with no parameters are likely to be accessing global data.
Objects visible within package specifications can be modified by any unit that
has visibility to them. The object cannot be protected or represented
abstractly by its enclosing package. Objects which must persist should be
declared in package bodies. Objects whose value depends on program units
external to their enclosing package are probably either in the wrong package
or are better accessed by a subprogram specified in the package specification.
note
The specifications of some packages, such as Ada bindings to existing
subroutine libraries, cannot easily be reduced in size. In such cases, it may
be beneficial to break these up into smaller packages, grouping according to
category (e.g., trigonometric functions).
4.2.2 Nested Packages
guideline
o Nest package specifications within another package specification only
for grouping operations, hiding common implementation details, or presenting
different views of the same abstraction.
example
Chapter 14 of the Ada Language Reference Manual gives an example of desirable
package specification nesting. The specifications of generic packages
Integer_IO, Float_IO, Fixed_IO, and Enumeration_IO are nested within the
specification of package Text_IO. Each of them is a generic, grouping closely
related operations and needing to use hidden details of the implementation of
Text_IO.
rationale
Grouping package specifications into an encompassing package emphasizes a
relationship of commonality among those packages. It also allows them to share
common implementation details resulting from the relationship.
An abstraction occasionally needs to present different views to different
classes of users. Building one view upon another as an additional abstraction
does not always suffice, because the functionality of the operations presented
by the views may be only partially disjoint. Nesting specifications groups the
facilities of the various views, yet associates them with the abstraction they
present. Abusive mixing of the views by another unit would be easy to detect
due to the multiple use clauses or an incongruous mix of qualified names.
4.2.3 Restricting Visibility
guideline
o Restrict the visibility of program units as much as possible by
nesting them inside other program units and hiding them inside package bodies
(Nissen and Wallis 1984).
o Minimize the scope within which with clauses apply.
o Only with those units directly needed.
example
This program is a compiler. Access to the printing facilities of Text_IO is
restricted to the software involved in producing the source code listing.
-------------------------------------------------------------------------
procedure Compiler is
----------------------------------------------------------------------
package Listing_Facilities is
procedure New_Page_Of_Listing;
procedure New_Line_Of_Print;
...
end Listing_Facilities;
----------------------------------------------------------------------
package body Listing_Facilities is separate;
begin -- Compiler
...
end Compiler;
-------------------------------------------------------------------------
with Text_IO;
separate (Compiler)
package body Listing_Facilities is
----------------------------------------------------------------------
procedure New_Page_Of_Listing is
begin
...
end New_Page_Of_Listing;
----------------------------------------------------------------------
procedure New_Line_Of_Print is
begin
...
end New_Line_Of_Print;
...
end Listing_Facilities;
-------------------------------------------------------------------------
rationale
Restricting visibility of a program unit ensures that the program unit is not
called from some other part of the system than that which was intended. This
is done by nesting it inside of the only unit which uses it, or by hiding it
inside of a package body rather than declaring it in the package
specification. This avoids errors and eases the job of maintainers by
guaranteeing that a local change in that unit will not have an unforeseen
global effect.
Restricting visibility of a library unit, by using with clauses on subunits
rather than on the entire parent unit, is useful in the same way. In the
example above, it is clear that the package Text_IO is used only by the
Listing_Facilities package of the compiler.
note
One way to minimize the coverage of a with clause is to use it only with
subunits that really need it. Consider making those subunits separate
compilation units when the need for visibility to a library unit is restricted
to a subprogram or two.
4.2.4 Hiding Tasks
guideline
o Carefully consider encapsulation of tasks.
example
-------------------------------------------------------------------------
package Disk_Head_Scheduler is
type Words is ...
type Track_Number is ...
procedure Transmit (Track : in Track_Number;
Data : in Words);
...
end Disk_Head_Scheduler;
-------------------------------------------------------------------------
package body Disk_Head_Scheduler is
...
task Control is
entry Sign_In (Track : in Track_Number);
...
end Control;
----------------------------------------------------------------------
task Track_Manager is
entry Transfer(Track_Number) (Data : in Words);
end Track_Manager;
----------------------------------------------------------------------
...
procedure Transmit (Track : in Track_Number;
Data : in Words) is
begin
Control.Sign_In(Track);
Track_Manager.Transfer(Track)(Data);
end Transmit;
----------------------------------------------------------------------
...
end Disk_Head_Scheduler;
-------------------------------------------------------------------------
rationale
The decision whether to declare a task in the specification or body of an
enclosing package is not a simple one. There are good arguments for both.
Hiding a task specification in a package body and exporting (via subprograms)
only required entries reduces the amount of extraneous information in the
package specification. It allows your subprograms to enforce any order of
entry calls necessary to the proper operation of the tasks. It also allows you
to impose defensive task communication practices (see Guideline ) and proper
use of conditional and timed entry calls. Finally, it allows the grouping of
entries into sets for export to different classes of users (e.g., producers
versus consumers), or the concealment of entries that should not be made
public at all (e.g., initialization, completion, signals). Where performance
is an issue and there are no ordering rules to enforce, the entries can be
renamed as subprograms to avoid the overhead of an extra procedure call.
An argument which can be viewed as an advantage or disadvantage is that hiding
the task specification in a package body hides the fact of a tasking
implementation from the user. If the application is such that a change to or
from a tasking implementation, or a reorganization of services among tasks,
need not concern users of the package then this is an advantage. However, if
the package user must know about the tasking implementation to reason about
global tasking behavior, then it is better not to hide the task completely.
Either move it to the package specification or add comments stating that there
is a tasking implementation, describing when a call may block, etc. Otherwise,
it is the package implementor's responsibility to ensure that users of the
package do not have to concern themselves with behaviors such as deadlock,
starvation, and race conditions.
Finally, keep in mind that hiding tasks behind a procedural interface prevents
the usage of conditional and timed entry calls and entry families, unless you
add parameters and extra code to the procedures to make it possible for
callers to direct the procedures to use these capabilities.
4.3 EXCEPTIONS
This section addresses the issue of exceptions in the context of program
structures. It discusses how exceptions should be used as part of the
interface to a unit, including what exceptions to declare and raise and under
what conditions to raise them. Information on how to handle, propagate, and
avoid raising exceptions is found in Guideline . Guidelines on how to deal
with portability issues are in Guideline .
4.3.1 Using Exceptions to Help Define an Abstraction
guideline
o Declare a different exception name for each error that the user of a
unit can make.
o Declare a different exception name for each unavoidable and
unrecoverable internal error which can occur in a unit.
o Do not borrow an exception name from another context.
o Export (declare visibly to the user) the names of all exceptions which
can be raised.
o In a package, document which exceptions can be raised by each
subprogram and task entry.
o Do not raise exceptions for internal errors which can be avoided or
corrected within the unit.
o Do not raise the same exception to report different types of errors
which are distinguishable by the user of the unit.
o Provide interrogative functions which allow the user of a unit to
avoid causing exceptions to be raised.
o When possible, avoid changing state information in a unit before
raising an exception.
o Catch and convert or handle all predefined and compiler-defined
exceptions at the earliest opportunity.
o Do not explicitly raise predefined or implementation-defined
exceptions.
o Never let an exception propagate beyond its scope.
example
This package specification defines an exception which enhances the abstraction:
-------------------------------------------------------------------------
generic
type Element is private;
package Stack is
function Stack_Empty return Boolean;
-- Raised when POP is used on empty stack.
No_Data_On_Stack : exception;
procedure Pop (From_Top : out Element);
procedure Push (Onto_Top : in Element);
end Stack;
-------------------------------------------------------------------------
This example shows how to convert a predefined exception to a user-defined one:
...
----------------------------------------------------------------------
procedure Pop (From_Top : out Element) is
...
if Stack_Empty then
raise No_Data_On_Stack;
else -- Stack contains at least one element
Top_Index := Top_Index - 1;
From_Top := Stack(Top_Index + 1);
end if;
end Pop;
----------------------------------------------------------------------
...
rationale
Exceptions should be used as part of an abstraction to indicate error
conditions which the abstraction is unable to prevent or correct. Since the
abstraction is unable to correct such an error, it must report the error to
the user. In the case of a usage error (e.g., attempting to invoke operations
in the wrong sequence or attempting to exceed a boundary condition), the user
may be able to correct the error. In the case of an error beyond the control
of the user, the user may be able to work around the error if there are
multiple mechanisms available to perform the desired operation. In other
cases, the user may have to abandon use of the unit, dropping into a degraded
mode of limited functionality. In any case, the user must be notified.
Exceptions are a good mechanism for reporting such errors because they provide
an alternate flow of control for dealing with errors. This allows
error-handling code to be kept separate from the code for normal processing.
When an exception is raised, the current operation is aborted and control is
transferred directly to the appropriate exception handler.
Several of the guidelines above exist to maximize the ability of the user to
distinguish and correct different types of errors. Providing a different
exception name for each error condition makes it possible to handle each error
condition separately. Declaring new exception names, rather than raising
exceptions declared in other packages, reduces the coupling between packages
and also makes different exceptions more distinguishable. Exporting the names
of all exceptions which a unit can raise, rather than declaring them
internally to the unit, makes it possible for users of the unit to refer to
the names in exception handlers. Otherwise, the user would be able to handle
the exception only with an others handler. Finally, using comments to document
exactly which of the exceptions declared in a package can be raised by each
subprogram or task entry making it possible for the user to know which
exception handlers are appropriate in each situation.
Because they cause an immediate transfer of control, exceptions are useful for
reporting unrecoverable errors which prevent an operation from being
completed, but not for reporting status or modes incidental to the completion
of an operation. They should not be used to report internal errors which a
unit was able to correct invisibly to the user.
To provide the user with maximum flexibility, it is a good idea to provide
interrogative functions which the user can call to determine whether an
exception would be raised if a subprogram or task entry were invoked. The
function Stack_Empty in the above example is such a function. It indicates
whether No_Data_On_Stack would be raised if Pop were called. Providing such
functions makes it possible for the user to avoid triggering exceptions.
To support error recovery by its user, a unit should try to avoid changing
state during an invocation which raises an exception. If a requested operation
cannot be completely and correctly performed, then the unit should either
detect this before changing any internal state information, or should revert
back to the state at the time of the request. For example, after raising the
exception No_Data_On_Stack, the stack package in the above example should
remain in exactly the same state it was in when Pop was called. If it were to
partially update its internal data structures for managing the stack, then
future Push and Pop operations would not perform correctly. This is always
desirable, but not always possible.
User-defined exceptions should be used instead of predefined or
compiler-defined exceptions because they are more descriptive and more specific
to the abstraction. The predefined exceptions are very general, and can be
triggered by many different situations. Compiler-defined exceptions are
nonportable and have meanings which are subject to change even between
successive releases of the same compiler. This introduces too much uncertainty
for the creation of useful handlers.
If you are writing an abstraction, remember that the user does not know about
the units you use in your implementation. That is an effect of information
hiding. If any exception is raised within your abstraction, you must catch it
and handle it. The user is not able to provide a reasonable handler if the
original exception is allowed to propagate out. You can still convert the
exception into a form intelligible to the user if your abstraction cannot
effectively recover on its own.
Converting an exception means raising a user-defined exception in the handler
for the original exception. This introduces a meaningful name for export to
the user of the unit. Once the error situation is couched in terms of the
application, it can be handled in those terms.
Do not allow an exception to propagate unhandled outside the scope of the
declaration of its name, because then only a handler for others can catch it.
As discussed under Guideline , a handler for others cannot be written to deal
with the specific error effectively.
4.4 SUMMARY
high-level structure
o Place the specification of each library unit package in a separate
file from its body.
o Create an explicit specification, in a separate file, for each library
unit subprogram.
o Use subunits for the bodies of large units which are nested in other
units.
o Place each subunit in a separate file.
o Use a consistent file naming convention.
o Use subprograms to enhance abstraction.
o Restrict each subprogram to the performance of a single action.
o Use a function when the subprogram's primary purpose is to provide a
single value.
o Minimize the side effect of a function.
o Use packages for information hiding.
o Use packages with private types for abstract data types.
o Use packages to model abstract entities appropriate to the problem
domain.
o Use packages to group together related type and object declarations
(e.g., common declarations for two or more library units).
o Use packages to group together related program units for configuration
control or visibility reasons.
o Encapsulate machine dependencies in packages. Place a software
interface to a particular device in a package to facilitate a change to a
different device.
o Place low-level implementation decisions or interfaces in subprograms
within packages.
o Use packages and subprograms to encapsulate and hide program details
that may change.
o Make each package serve a single purpose.
o Use packages to group functionally related data, types, and
subprograms.
o Avoid collections of unrelated objects and subprograms.
o Avoid putting variables in package specifications.
o Use tasks to model abstract, asynchronous entities within the problem
domain.
o Use tasks to control or synchronize access to tasks or other
asynchronous entities (e.g., asynchronous I/O, peripheral devices, interrupts).
o Use tasks to define concurrent algorithms for multiprocessor
architectures.
o Use tasks to perform concurrent, cyclic, or prioritized activities.
visibility
o Put only what is needed for the use of a package into its
specification.
o Minimize the number of declarations in package specifications.
o Do not include extra operations simply because they are easy to build.
o Minimize the context (with) clauses in a package specification.
o Reconsider subprograms which seem to require large numbers of
parameters.
o Do not manipulate global data within a subprogram or package merely to
limit the number of parameters.
o Avoid unnecessary visibility; hide the implementation details of a
program unit from its users.
o Nest package specifications within another package specification only
for grouping operations, hiding common implementation details, or presenting
different views of the same abstraction.
o Restrict the visibility of program units as much as possible by
nesting them inside other program units and hiding them inside package bodies.
o Minimize the scope within which with clauses apply.
o Only with those units directly needed.
o Carefully consider encapsulation of tasks.
exceptions
o Declare a different exception name for each error that the user of a
unit can make.
o Declare a different exception name for each unavoidable and
unrecoverable internal error which can occur in a unit.
o Do not borrow an exception name from another context.
o Export (declare visibly to the user) the names of all exceptions which
can be raised.
o In a package, document which exceptions can be raised by each
subprogram and task entry.
o Do not raise exceptions for internal errors which can be avoided or
corrected within the unit.
o Do not raise the same exception to report different types of errors
which are distinguishable by the user of the unit.
o Provide interrogative functions which allow the user of a unit to
avoid causing exceptions to be raised.
o When possible, avoid changing state information in a unit before
raising an exception.
o Catch and convert or handle all predefined and compiler-defined
exceptions at the earliest opportunity.
o Do not explicitly raise predefined or implementation-defined
exceptions.
o Never let an exception propagate beyond its scope.
CHAPTER 5
Programming Practices
Software is always subject to change. The need for this change,
euphemistically known as "maintenance" arises from a variety of sources.
Errors need to be corrected as they are discovered. System functionality may
need to be enhanced in planned or unplanned ways. Inevitably, the requirements
change over the lifetime of the system, forcing continual system evolution.
Often, these modifications are conducted long after the software was
originally written, usually by someone other than the original author.
Easy and successful modification requires that the software be readable,
understandable, and structured according to accepted practice. If a software
component cannot be easily understood by a programmer who is familiar with its
intended function, that software component is not maintainable. Techniques
that make code readable and comprehensible enhance its maintainability.
Previous chapters presented techniques such as consistent use of naming
conventions, clear and well-organized commentary, and proper modularization.
This chapter presents consistent and logical use of language features.
Correctness is one aspect of reliability. While style guidelines cannot
enforce the use of correct algorithms, they can suggest the use of techniques
and language features known to reduce the number or likelihood of failures.
Such techniques include program construction methods that reduce the
likelihood of errors or that improve program predictability by defining
behavior in the presence of errors.
5.1 OPTIONAL PARTS OF THE SYNTAX
Parts of the Ada syntax, while optional, can enhance the readability of the
code. The guidelines given below concern use of some of these optional
features.
5.1.1 Loop Names
guideline
o Associate names with loops when they are nested (Booch 1987, 1986).
example
Process_Each_Page:
loop
Process_All_The_Lines_On_This_Page:
loop
...
exit Process_All_The_Lines_On_This_Page when
Line_Number = Max_Lines_On_Page;
...
Look_For_Sentinel_Value:
loop
...
exit Look_For_Sentinel_Value when
Current_Symbol = Sentinel;
...
end loop Look_For_Sentinel_Value;
...
end loop Process_All_The_Lines_On_This_Page;
...
exit Process_Each_Page when Page_Number = Maximum_Pages;
...
end loop Process_Each_Page;
rationale
When you associate a name with a loop, you must include that name with the
associated end for that loop (Department of Defense 1983). This helps readers
find the associated end for any given loop. This is especially true if loops
are broken over screen or page boundaries. The choice of a good name for the
loop documents its purpose, reducing the need for explanatory comments. If a
name for a loop is very difficult to choose, this could indicate a need for
more thought about the algorithm.
Regularly naming loops helps you follow Guideline .
It can be difficult to think up a name for every loop, therefore the guideline
specifies nested loops. The benefits in readability and second thought
outweigh the inconvenience of naming the loops.
5.1.2 Block Names
guideline
o Associate names with blocks when they are nested.
example
Trip:
declare
...
begin -- Trip
Arrive_At_Airport:
declare
...
begin -- Arrive_At_Airport
Rent_Car;
Claim_Baggage;
Reserve_Hotel;
...
end Arrive_At_Airport;
Visit_Customer:
declare
...
begin -- Visit_Customer
-- again a set of activities...
...
end Visit_Customer;
Departure_Preparation:
declare
...
begin -- Departure_Preparation
Return_Car;
Check_Baggage;
Wait_For_Flight;
...
end Departure_Preparation;
Board_Return_Flight;
end Trip;
rationale
When there is a nested block structure it can be difficult to determine which
end corresponds to which block. Naming blocks alleviates this confusion. The
choice of a good name for the block documents its purpose, reducing the need
for explanatory comments. If a name for the block is very difficult to choose,
this could indicate a need for more thought about the algorithm.
This guideline is also useful if nested blocks are broken over a screen or
page boundary.
It can be difficult to think up a name for each block, therefore the guideline
specifies nested blocks. The benefits in readability and second thought
outweigh the inconvenience of naming the blocks.
5.1.3 Exit Statements
guideline
o Use loop names on all exit statements from nested loops.
example
See the example in Guideline .
rationale
An exit statement is an implicit goto. It should specify its source
explicitly. When there is a nested loop structure and an exit statement is
used, it can be difficult to determine which loop is being exited. Also,
future changes which may introduce a nested loop are likely to introduce an
error, with the exit accidentally exiting from the wrong loop. Naming loops
and their exits alleviates this confusion. This guideline is also useful if
nested loops are broken over a screen or page boundary.
5.1.4 Naming End Statements
guideline
o Include the simple name at the end of a package specification and body.
o Include the simple name at the end of a task specification and body.
o Include the simple name at the end of an accept statement.
o Include the designator at the end of a subprogram body.
example
------------------------------------------------------------------------
package Autopilot is
function Is_Engaged return Boolean;
procedure Engage;
procedure Disengage;
end Autopilot;
------------------------------------------------------------------------
package body Autopilot is
...
---------------------------------------------------------------------
task Course_Monitor is
entry Reset (Engage : in Boolean);
end Course_Monitor;
---------------------------------------------------------------------
function Is_Engaged return Boolean is
...
end Is_Engaged;
---------------------------------------------------------------------
procedure Engage is
...
end Engage;
---------------------------------------------------------------------
procedure Disengage is
...
end Disengage;
---------------------------------------------------------------------
task body Course_Monitor is
...
accept Reset (Engage : in Boolean) do
...
end Reset;
...
end Course_Monitor;
---------------------------------------------------------------------
end Autopilot;
------------------------------------------------------------------------
rationale
Repeating names on the end of these compound statements ensures consistency
throughout the code. In addition, the named end provides a reference for the
reader if the unit spans a page or screen boundary, or if it contains a nested
unit.
5.2 PARAMETER LISTS
A subprogram or entry parameter list is the interface to the abstraction
implemented by the subprogram or entry. It is important that it is clear and
that it is expressed in a consistent style. Careful decisions about formal
parameter naming and ordering can make the purpose of the subprogram easier to
understand which can make it easier to use.
5.2.1 Formal Parameters
guideline
o Name formal parameters descriptively to reduce the need for comments.
example
List_Manager.Insert (Element => New_Employee,
Into_List => Probationary_Employees,
At_Position => 1);
rationale
Following the variable naming guidelines (Guidelines and ) for formal
parameters can make calls to subprograms read more like regular prose, as
shown in the example above where no comments are necessary. Descriptive names
of this sort can also make the code in the body of the subprogram more clear.
5.2.2 Named Association
guideline
o Use named parameter association in calls of infrequently used
subprograms or entries with many formal parameters.
o Use named association when instantiating generics.
o Use named association for clarification when the actual parameter is
any literal or expression.
o Use named association when supplying a nondefault value to an optional
parameter.
instantiation
- Use named parameter association in calls of subprograms or entries
called from less than five places in a single source file or with more than
two formal parameters.
example
Encode_Telemetry_Packet (Source => Power_Electronics,
Content => Temperature,
Value =>
Read_Temperature_Sensor(Power_Electronics),
Time => Current_Time,
Sequence => Next_Packet_ID,
Vehicle => This_Spacecraft,
Primary_Module => True);
rationale
Calls of infrequently used subprograms or entries with many formal parameters
can be difficult to understand without referring to the subprogram or entry
code. Named parameter association can make these calls more readable.
When the formal parameters have been named appropriately, it is easier to
determine exactly what purpose the subprogram serves without looking at its
code. This reduces the need for named constants that exist solely to make
calls more readable. It also allows variables used as actual parameters to be
given names indicating what they are without regard to why they are being
passed in a call. An actual parameter, which is an expression rather than a
variable, cannot be named otherwise.
Named association allows subprograms to have new parameters inserted with
minimal ramifications to existing calls.
note
The judgment of when named parameter association improves readability is
subjective. Certainly, simple or familiar subprograms such as a swap routine
or a sine function do not require the extra clarification of named association
in the procedure call.
caution
A consequence of named parameter association is that the formal parameter
names may not be changed without modifying the text of each call.
5.2.3 Default Parameters
guideline
o Provide default parameters to allow for occasional, special use of
widely used subprograms or entries.
o Place default parameters at the end of the formal parameter list.
o Consider providing default values to new parameters added to an
existing subprogram.
example
Chapter 14 of the Ada Language Reference Manual (Department of Defense 1983)
contains many examples of this practice.
rationale
Often, the majority of uses of a subprogram or entry need the same value for a
given parameter. Providing that value, as the default for the parameter, makes
the parameter optional on the majority of calls. It also allows the remaining
calls to customize the subprogram or entry by providing different values for
that parameter.
Placing default parameters at the end of the formal parameter list allows the
caller to use positional association on the call, otherwise defaults are
available only when named association is used.
Often during maintenance activities, you increase the functionality of a
subprogram or entry. This requires more parameters than the original form for
some calls. New parameters may be required to control this new functionality.
Give the new parameters default values which specify the old functionality.
Calls needing the old functionality need not be changed; they take the
defaults. This is true if the new parameters are added to the end of the
parameter list, or if named association is used on all calls. New calls
needing the new functionality can specify that by providing other values for
the new parameters.
This enhances maintainability in that the places which use the modified
routines do not themselves have to be modified, while the previous
functionality levels of the routines are allowed to be "reused."
exceptions
Do not go overboard. If the changes in functionality are truly radical, you
should be preparing a separate routine rather than modifying an existing one.
One indicator of this situation would be that it is difficult to determine
value combinations for the defaults that uniquely and naturally require the
more restrictive of the two functions. In such cases, it is better to go ahead
with creation of a separate routine.
5.2.4 Mode Indication
guideline
o Show the mode indication of all procedure and entry parameters (Nissen
and Wallis 1984).
o Use in out only when the parameter is both read from and updated.
example
procedure Open_File (File_Name : in String;
Open_Status : out Status_Codes);
entry Acquire (Key : in Capability;
Resource : out Tape_Drive);
rationale
By showing the mode of parameters, you aid the reader. If you do not specify a
parameter mode, the default mode is in. Explicitly showing the mode indication
of all parameters is a more assertive action than simply taking the default
mode. Anyone reviewing the code later will be more confident that you intended
the parameter mode to be in.
Use the mode that reflects the actual use of the parameter. Only use in out
mode when reading and writing to a parameter.
exception
It may be necessary to consider several alternative implementations for a
given abstraction. For example, a bounded stack can be implemented as a
pointer to an array. Even though an update to the object being pointed to
does not require changing the pointer value itself, you may want to consider
making the mode in out to allow changes to the implementation and to document
more accurately what the operation is doing. If you later change the
implementation to a simple array, the mode will have to be in out, potentially
causing changes to all places that the routine is called.
5.3 TYPES
In addition to determining the possible values for variables, type names, and
distinctions can be very valuable aids in developing safe, readable, and
understandable code. Types clarify the structure of your data and can limit or
restrict the operations performed on that data. "Keeping types distinct has
been found to be a very powerful means of detecting logical mistakes when a
program is written and to give valuable assistance whenever the program is
being subsequently maintained." (Pyle 1985) Take advantage of Ada's strong
typing capability in the form of subtypes, derived types, task types, private
types, and limited private types.
The guidelines encourage much code to be written to ensure strong typing
(i.e., subtypes). While it might appear that there would be execution
penalties for this amount of code, this is usually not the case. In contrast
to other conventional languages, Ada has a less direct relationship between
the amount of code that is written and the size of the resulting executable
program. Most of the strong type checking is performed at compilation time
rather than execution time, so the size of the executable code is not greatly
affected.
5.3.1 Derived Types and Subtypes
guideline
o Use existing types as building blocks by deriving new types from them.
o Use range constraints on subtypes.
o Define new types, especially derived types, to include the largest set
of possible values, including boundary values.
o Constrain the ranges of derived types with subtypes, excluding
boundary values.
example
Type Table is a building block for the creation of new types:
type Table is
record
Count : List_Size := Empty;
List : Entry_List := Empty_List;
end record;
type Telephone_Directory is new Table;
type Department_Inventory is new Table;
The following are distinct types that cannot be intermixed in operations that
are not programmed explicitly to use them both:
type Dollars is new Number;
type Cents is new Number;
Below, Source_Tail has a value outside the range of Listing_Paper when the
line is empty. All the indices can be mixed in expressions, as long as the
results fall within the correct subtypes:
type Columns is range First_Column - 1 .. Listing_Width + 1;
subtype Listing_Paper is
Columns range First_Column .. Listing_Width;
subtype Dumb_Terminal is
Columns range First_Column .. Dumb_Terminal_Width;
type Line is array (Columns range <>) of Bytes;
subtype Listing_Line is Line (Listing_Paper);
subtype Terminal_Line is Line (Dumb_Terminal);
Source_Tail : Columns := Columns'First;
Source : Listing_Line;
Destination : Terminal_Line;
...
Destination(Destination'First .. Source_Tail - Destination'Last) :=
Source(Columns'Succ(Destination'Last) .. Source_Tail);
rationale
The name of a derived type can make clear its intended use and avoid
proliferation of similar type definitions. Objects of two derived types, even
though derived from the same type, cannot be mixed in operations unless such
operations are supplied explicitly or one is converted to the other
explicitly. This prohibition is an enforcement of strong typing.
Define new types, derived types, and subtypes cautiously and deliberately. The
concepts of subtype and derived type are not equivalent, but they can be used
to advantage in concert. A subtype limits the range of possible values for a
type, but does not define a new type.
Types can have highly constrained sets of values without eliminating useful
values. Used in concert, derived types and subtypes can eliminate many flag
variables and type conversions within executable statements. This renders the
program more readable, enforces the abstraction, and allows the compiler to
enforce strong typing constraints.
Many algorithms begin or end with values just outside the normal range. If
boundary values are not compatible within subexpressions, algorithms can be
needlessly complicated. The program can become cluttered with flag variables
and special cases when it could just test for zero or some other sentinel
value just outside normal range.
The type Columns and the subtype Listing_Paper in the example above
demonstrate how to allow sentinel values. The subtype Listing_Paper could be
used as the type for parameters of subprograms declared in the specification
of a package. This would restrict the range of values which could be
specified by the caller. Meanwhile, the type COLUMNS could be used to store
such values internally to the body of the package, allowing First_Column - 1
to be used as a sentinel value. This combination of types and subtypes allows
compatibility between subtypes within subexpressions without type conversions
as would happen with derived types.
note
The price of the reduction in the number of independent type declarations is
that subtypes and derived types change when the base type is redefined. This
trickle-down of changes is sometimes a blessing and sometimes a curse. However,
usually it is intended and beneficial.
5.3.2 Anonymous Types
guideline
o Avoid anonymous types.
o Use anonymous types for array variables when no suitable type exists
and the array will not be referenced as a whole.
example
Use
type Buffer_Index is range 1 .. 80;
type Buffer is array (Buffer_Index) of Character;
Input_Line : Buffer;
rather than
Input_Line : array (Buffer_Index) of Character;
rationale
Although Ada allows anonymous types, they have limited usefulness and
complicate program modification. For example, except for arrays, a variable of
anonymous type can never be used as an actual parameter because it is not
possible to define a formal parameter of the same type. Even though this may
not be a limitation initially, it precludes a modification in which a piece of
code is changed to a subprogram. Also, two variables declared using the same
anonymous type declaration are actually of different types.
Even though the implicit conversion of array types during parameter passing is
supported in Ada, it is difficult to justify not using the type of the
parameter. In most situations, the type of the parameter is visible and
easily substituted in place of an anonymous array type. The use of an
anonymous array type implies that the array is only being used as a convenient
way to implement a collection of values. It is misleading to use an anonymous
type and then treat the variable as an object.
note
For anonymous task types, see Guideline .
In reading the Ada Language Reference Manual (Department of Defense 1983), you
will notice that there are cases when anonymous types are mentioned abstractly
as part of the description of the Ada computational model. These cases do not
violate this guideline.
5.3.3 Private Types
guideline
o Use limited private types in preference to private types.
o Use private types in preference to nonprivate types.
o Explicitly export needed operations rather than easing restrictions.
example
------------------------------------------------------------------------
package Packet_Telemetry is
type Frame_Header is limited private;
type Frame_Data is private;
type Frame_Codes is (Main_Bus_Voltage, Transmitter_1_Power);
...
private
type Frame_Header is
record
...
end record;
type Frame_Data is
record
...
end record;
...
end Packet_Telemetry;
------------------------------------------------------------------------
rationale
Limited private types and private types support abstraction and information
hiding better than nonprivate types. The more restricted the type, the better
information hiding is served. This, in turn, allows the implementation to
change without affecting the rest of the program. While there are many valid
reasons to export types, it is better to try the preferred route first,
loosening the restrictions only as necessary. If it is necessary for a user of
the package to use a few of the restricted operations, it is better to export
the operations explicitly and individually via exported subprograms than to
drop a level of restriction. This practice retains the restrictions on other
operations.
Limited private types have the most restricted set of operations available to
users of a package. Of the types that must be made available to users of a
package, as many as possible should be limited private. The operations
available to limited private types are membership tests, selected components,
components for the selections of any discriminant, qualification and explicit
conversion, and attributes 'Base and 'Size. Objects of a limited private type
also have the attribute 'Constrained if there are discriminants. None of these
operations allow the user of the package to manipulate objects in a way that
depends on the structure of the type.
If additional operations must be available to the type, the restrictions may
be loosened by making it a private type. The operations available on objects
of nprivate types that are not available on objects of limited private types
are assignment and tests for equality and inequality. There are advantages to
the restrictive nature of limited private types. For example, assignment
allows copies of an object to be made. This could be a problem if the object's
type is a pointer.
note
The predefined packages Direct_IO and Sequential_IO do not accept limited
private types as generic parameters. This restriction should be considered
when I/O operations are needed for a type.
5.4 DATA STRUCTURES
The data structuring capabilities of Ada are a powerful resource; therefore,
nuse them to model the data as closely as possible. It is possible to group
logically related data and let the language control the abstraction and
operations on the data rather than requiring the programmer or maintainer to
do so. Data can also be organized in a building block fashion. In addition to
showing how a data structure is organized (and possibly giving the reader an
indication as to why it was organized that way), creating the data structure
from smaller components allows those components to be reused themselves. Using
the features that Ada provides can increase the maintainability of your code.
5.4.1 Heterogeneous Data
guideline
o Use records to group heterogeneous but related data.
o Consider records to map to I/O device data.
example
type Propulsion_Method is (Sail, Diesel, Nuclear);
type Craft is
record
Name : Common_Name;
Plant : Propulsion_Method;
Length : Feet;
Beam : Feet;
Draft : Feet;
end record;
type Fleet is array (1 .. Fleet_Size) of Craft;
rationale
You help the maintainer find all of the related data by gathering it into the
same construct, simplifying any modifications that apply to all rather than
part. This in turn increases reliability. Neither you nor an unknown maintainer
are liable to forget to deal with all the pieces of information in the
executable statements, especially if updates are done with aggregate
assignments whenever possible.
The idea is to put the information a maintainer needs to know where it can be
found with the minimum of effort. For example, if all information relating to
a given Craft is in the same place, the relationship is clear both in the
declarations and especially in the code accessing and updating that
information. But, if it is scattered among several data structures, it is less
obvious that this is an intended relationship as opposed to a coincidental
one. In the latter case, the declarations may be grouped together to imply
intent, but it may not be possible to group the accessing and updating code
that way. Ensuring the use of the same index to access the corresponding
element in each of several parallel arrays is difficult if the accesses are at
all scattered.
If the application must interface directly to hardware, the use of records,
especially in conjunction with record representation clauses, could be useful
to map onto the layout of the hardware in question.
note
It may seem desirable to store heterogeneous data in parallel arrays in what
amounts to a FORTRAN-like style. This style is an artifact of FORTRAN's data
structuring limitations. FORTRAN only has facilities for constructing
homogeneous arrays. Ada's variant record types offer one way to specify what
are called nonhomogeneous arrays or heterogeneous arrays.
exceptions
If the application must interface directly to hardware, and the hardware
requires that information be distributed among various locations, then it may
not be possible to use records.
5.4.2 Nested Records
guideline
o Record structures should not always be flat. Factor out common parts.
o For a large record structure, group related components into smaller
subrecords.
o For nested records, pick element names that read well when inner
elements are referenced.
example
type Coordinate is
record
Row : Local_Float;
Column : Local_Float;
end record;
type Window is
record
Top_Left : Coordinate;
Bottom_Right : Coordinate;
end record;
rationale
You can make complex data structures understandable and comprehensible by
composing them of familiar building blocks. This technique works especially
well for large record types with parts which fall into natural groupings. The
components factored into separately declared records, based on a common
quality or purpose, correspond to a lower level of abstraction than that
represented by the larger record.
note
A carefully chosen name for the component of the larger record that is used to
select the smaller enhances readability, for example:
if Window1.Bottom_Right.Row > Window2.Top_Left.Row then . . .
5.4.3 Dynamic Data
guideline
o Differentiate between static and dynamic data. Use dynamically
allocated objects with caution.
o Use dynamically allocated data structures only when it is necessary to
create and destroy them dynamically or to be able to reference them by
different names.
o Do not drop pointers to undeallocated objects.
o Do not leave dangling references to deallocated objects.
o Initialize all access variables and components within a record.
o Do not rely on memory deallocation.
o Deallocate explicitly.
o Use length clauses to specify total allocation size.
o Provide handlers for Storage_Error.
example
These lines show how a dangling reference might be created:
P1 := new Object;
P2 := P1;
Unchecked_Object_Deallocation(P2);
This line can raise an exception due to referencing the deallocated object:
X := P1.all;
In the following three lines, if there is no intervening assignment of the
value of P1 to any other pointer, the object created on the first line is no
longer accessible after the third line. The only pointer to the allocated
object has been dropped.
P1 := new Object;
...
P1 := P2;
rationale
See also Guidelines , , and for variations on these problems. A dynamically
allocated object is an object created by the execution of an allocator
("new"). Allocated objects referenced by access variables allow you to
generate aliases, which are multiple references to the same object. Anomalous
behavior can arise when you reference a deallocated object by another name.
This is called a dangling reference. Totally disassociating a still-valid
object from all names is called dropping a pointer. A dynamically allocated
object that is not associated with a name cannot be referenced or explicitly
deallocated.
A dropped pointer depends on an implicit memory manager for reclamation of
space. It also raises questions for the reader as to whether the loss of
access to the object was intended or accidental.
An Ada environment is not required to provide deallocation of dynamically
allocated objects. If provided, it may be provided implicitly (objects are
deallocated when their access type goes out of scope), explicitly (objects are
deallocated when Unchecked_Deallocation is called), or both. To increase the
likelihood of the storage space being reclaimed, it is best to call
Unchecked_Deallocation explicitly for each dynamically object when you are
finished using it. Calls to Unchecked_Deallocation also document a deliberate
decision to abandon an object, making the code easier to read and understand.
To be absolutely certain that space is reclaimed and reused, manage your own
"free list." Keep track of which objects you are finished with, and reuse them
instead of dynamically allocating new objects later.
The dangers of dangling references are that you may attempt to use them,
thereby accessing memory which you have released to the memory manager, and
which may have been subsequently allocated for another purpose in another part
of your program. When you read from such memory, unexpected errors may occur
because the other part of your program may have previously written totally
unrelated data there. Even worse, when you write to such memory you can cause
errors in an apparently unrelated part of the code by changing values of
variables dynamically allocated by that code. This type of error can be very
difficult to find. Finally, such errors may be triggered in parts of your
environment that you didn't write, for example, in the memory management
system itself which may dynamically allocate memory to keep records about your
dynamically allocated memory.
Keep in mind that any uninitialized or unreset component of a record or array
can also be a dangling reference or carry a bit pattern representing
inconsistent data.
Whenever you use dynamic allocation it is possible to run out of space. Ada
provides a facility (a length clause) for requesting the size of the pool of
allocation space at compile time. Anticipate that you can still run out at run
time. Prepare handlers for the exception Storage_Error, and consider carefully
what alternatives you may be able to include in the program for each such
situation.
There is a school of thought that dictates avoidance of all dynamic
allocation. It is largely based on the fear of running out of memory during
execution. Facilities such as length clauses and exception handlers for
Storage_Error provide explicit control over memory partitioning and error
recovery, making this fear unfounded.
5.5 EXPRESSIONS
Properly coded expressions can enhance the readability and understandability
of a program. Poorly coded expressions can turn a program into a maintainer's
nightmare.
5.5.1 Range Values
guideline
o Use 'First or 'Last instead of numeric literals to represent the first
or last values of a range.
o Use the type or subtype name of the range instead of 'First .. 'Last.
example
type Temperature is range All_Time_Low .. All_Time_High;
type Weather_Stations is range 1 .. Max_Stations;
Current_Temperature : Temperature := 60;
Offset : Temperature;
...
for I in Weather_Stations loop
Offset := Current_Temperature - Temperature'First;
...
end loop;
rationale
In the example above, it is better to use Weather_Stations in the for loop
than to use Weather_Stations'First .. Weather_Stations'Last or 1 ..
Max_Stations, because it is clearer, less error-prone, and less dependent on
the definition of the type Weather_Stations. Similarly, it is better to use
Temperature'First in the offset calculation than to use All_Time_Low, because
the code will still be correct if the definition of the subtype Temperature is
changed. This enhances program reliability.
caution
When you implicitly specify ranges and attributes like this, be careful that
you use the correct type or subtype name. It is easy to refer to a very large
range without realizing it. For example, given the declarations:
type Large_Range is new Integer;
subtype Small_Range is Large_Range range 1 .. 10;
then the first declaration below works fine, but the second one is probably an
accident and raises an exception on most machines because it is requesting a
huge array (indexed from the smallest integer to the largest one):
Array_1 : array (Small_Range) of Integer;
Array_2 : array (Large_Range) of Integer;
5.5.2 Array Attributes
guideline
o Use array attributes 'First, 'Last, or 'Length instead of numeric
literals for accessing arrays.
o Use the 'Range of the array instead of the name of the index subtype
to express a range.
o Use 'Range instead of 'First .. 'Last to express a range.
example
subtype Name_String is String (1 .. Name_Length);
File_Path : Name_String := (others => ' ');
...
for I in File_Path'Range loop
...
end loop;
rationale
In the example above, it is better to use Name_String'Range in the for loop
than to use Name_String_Size, Name_String'First .. Name_String'Last, or 1 ..
30, because it is clearer, less error-prone, and less dependent on the
definitions of Name_String and Name_String_Size. If Name_String is changed to
have a different index type, or if the bounds of the array are changed, this
will still work correctly. This enhances program reliability.
5.5.3 Parenthetical Expressions
guideline
o Use parentheses to specify the order of subexpression evaluation to
clarify expressions (NASA 1987).
o Use parentheses to specify the order of evaluation for subexpressions
whose correctness depends on left to right evaluation.
example
(1.5 * X**2)/A - (6.5*X + 47.0)
2*I + 4*Y + 8*Z + C
rationale
The Ada rules of operator precedence are defined in Section 4.5 of Department
of Defense (1983) and follow the same commonly accepted precedence of
algebraic operators. The strong typing facility in Ada combined with the
common precedence rules make many parentheses unnecessary. However, when an
uncommon combination of operators occurs, it may be helpful to add parentheses
even when the precedence rules apply. The expression,
5 + ((Y ** 3) mod 10)
is clearer, and equivalent to
5 + Y**3 mod 10
The rules of evaluation do specify left to right evaluation for operators with
the same precedence level. However, it is the most commonly overlooked rule
of evaluation when checking expressions for correctness.
5.5.4 Positive Forms of Logic
guideline
o Avoid names and constructs that rely on the use of negatives.
o Choose names of flags so they represent states that can be used in
positive form.
example
Use
if Operator_Missing then
rather than either
if not Operator_Found then
or
if not Operator_Missing then
rationale
Relational expressions can be more readable and understandable when stated in
a positive form. As an aid in choosing the name, consider that the most
frequently used branch in a conditional construct should be encountered first.
exception
There are cases in which the negative form is unavoidable. If the relational
expression better reflects what is going on in the code, then inverting the
test to adhere to this guideline is not recommended.
5.5.5 Short Circuit Forms of the Logical Operators
guideline
o Use short-circuit forms of the logical operators.
example
Use
if Y /= 0 or else (X/Y) /= 10 then
or
if Y /= 0 then
if (X/Y) /= 10 then
rather than either
if Y /= 0 and (X/Y) /= 10 then
or to avoid Numeric_Error
if (X/Y) /= 10 then
Use
if Target /= null and then Target.Distance < Threshold then
rather than
if Target.Distance < Threshold then
to avoid referencing a field in a non-existent object.
rationale
The use of short-circuit control forms prevents a class of data-dependent
errors or exceptions that can occur as a result of expression evaluation. The
short-circuit forms guarantee an order of evaluation and an exit from the
sequence of relational expressions as soon as the expression's result can be
determined.
In the absence of short-circuit forms, Ada does not provide a guarantee of the
order of expression evaluation, nor does the language guarantee that
evaluation of a relational expression is abandoned when it becomes clear that
it evaluates to False (for and) or True (for or).
note
If it is important that all parts of a given expression always be evaluated,
the expression probably violates Guideline which limits side-effects in
functions.
5.5.6 Accuracy of Operations With Real Operands
guideline
o Use <= and >= in relational expressions with real operands instead of
=.
example
Current_Temperature : Temperature := 0.0;
Temperature_Increment : Temperature := 1.0 / 3.0;
Maximum_Temperature : constant := 100.0;
...
loop
...
Current_Temperature :=
Current_Temperature + Temperature_Increment;
...
exit when Current_Temperature >= Maximum_Temperature;
...
end loop;
rationale
Fixed and floating point values, even if derived from similar expressions, may
not be exactly equal. The imprecise, finite representations of real numbers in
hardware always have round-off errors so that any variation in the construction
path or history of two reals has the potential for resulting in different
numbers, even when the paths or histories are mathematically equivalent.
The Ada definition of model intervals also means that the use of <= is more
portable than either < or =.
note
Floating point arithmetic is treated in Guideline .
exceptions
If your application must test for an exact value of a real number (e.g.,
testing the precision of the arithmetic on a certain machine), then the =
would have to be used. But never use = on real operands as a condition to exit
a loop.
5.6 STATEMENTS
Careless or convoluted use of statements can make a program hard to read and
maintain even if its global structure is well organized. You should strive for
simple and consistent use of statements to achieve clarity of local program
structure. Some of the guidelines in this section counsel use or avoidance of
particular statements. As pointed out in the individual guidelines, rigid
adherence to those guidelines would be excessive, but experience has shown
that they generally lead to code with improved reliability and
maintainability.
5.6.1 Nesting
guideline
o Minimize the depth of nested expressions (Nissen and Wallis 1984).
o Minimize the depth of nested control structures (Nissen and Wallis
1984).
o Try simplification heuristics (see note).
instantiation
- Do not nest expressions or control structures beyond a nesting level
of five.
example
The following section of code:
if not Condition_1 then
if Condition_2 then
Action_A;
else -- not Condition_2
Action_B;
end if;
else -- Condition_1
Action_C;
end if;
can be rewritten more clearly and with less nesting as:
if Condition_1 then
Action_C;
elsif Condition_2 then
Action_A;
else -- not (Condition_1 or Condition_2)
Action_B;
end if;
rationale
Deeply nested structures are confusing, difficult to understand, and difficult
to maintain. The problem lies in the difficulty of determining what part of a
program is contained at any given level. For expressions, this is important in
achieving the correct placement of balanced grouping symbols and in achieving
the desired operator precedence. For control structures, the question involves
what part is controlled. Specifically, is a given statement at the proper
level of nesting, i.e., is it too deeply or too shallowly nested, or is the
given statement associated with the proper choice, e.g., for if or case
statements? Indentation helps, but it is not a panacea. Visually inspecting
alignment of indented code (mainly intermediate levels) is an uncertain job at
best. To minimize the complexity of the code, keep the maximum number of
nesting levels between three and five.
note
Ask yourself the following questions to help you simplify the code:
- Can some part of the expression be put into a constant or variable?
- Does some part of the lower nested control structures represent a
significant, and perhaps reusable computation that I can factor into a
subprogram?
- Can I convert these nested if statements into a case statement?
- Am I using else if where I could be using elsif?
- Can I reorder the conditional expressions controlling this nested
structure?
- Is there a different design that would be simpler?
exceptions
If deep nesting is required frequently, there may be overall design decisions
for the code that should be changed. Some algorithms require deeply nested
loops and segments controlled by conditional branches. Their continued use can
be ascribed to their efficiency, familiarity, and time proven utility. When
nesting is required, proceed cautiously and take special care with the choice
of identifiers and loop and block names.
5.6.2 Slices
guideline
o Use slices rather than a loop to copy part of an array.
example
First : constant Index := Index'First;
Second : constant Index := Index'Succ(First);
Third : constant Index := Index'Succ(Second);
type Vector is array (Index range <>) of Element;
subtype Column_Vector is Vector (Index);
type Square_Matrix is array (Index) of Column_Vector;
subtype Small_Range is Index range First .. Third;
subtype Diagonals is Vector (Small_Range);
type Tri_Diagonal is array (Index) of Diagonals;
Markov_Probabilities : Square_Matrix;
Diagonal_Data : Tri_Diagonal;
...
-- Remove diagonal and off diagonal elements.
Diagonal_Data(Index'First)(First) := Null_Value;
Diagonal_Data(Index'First)(Second .. Third) :=
Markov_Probabilities(Index'First)(First .. Second);
for I in Second .. Index'Pred(Index'Last) loop
Diagonal_Data(I) :=
Markov_Probabilities(I)(Index'Pred(I) .. Index'Succ(I));
end loop;
Diagonal_Data(Index'Last)(First .. Second) :=
Markov_Probabilities(Index'Last)
(Index'Pred(Index'Last) .. Index'Last);
Diagonal_Data(Index'Last)(Third) := Null_Value;
rationale
An assignment statement with slices is simpler and clearer than a loop, and
helps the reader see the intended action. Slice assignment can be faster than
a loop if a block move instruction is available.
5.6.3 Case Statements
guideline
o Never use an others choice in a case statement.
o Do not use ranges of enumeration literals in case statements.
example
type Color is (Red, Green, Blue, Purple);
Car_Color : Color := Red;
...
case Car_Color is
when Red .. Blue => ...
when Purple => ...
end case; -- Car_Color
Now consider a change in the type:
type Color is (Red, Yellow, Green, Blue, Purple);
This change may have an unnoticed and undesired effect in the case statement.
If the choices had been enumerated explicitly, as when Red | Green | Blue =>
instead of when Red .. Blue =>, then the case statement would have not have
compiled. This would have forced the maintainer to make a conscious decision
about what to do in the case of Yellow.
rationale
All possible values for an object should be known and should be assigned
specific actions. Use of an others clause may prevent the developer from
carefully considering the actions for each value. A compiler warns the user
about omitted values, if an others clause is not used.
Each possible value should be explicitly enumerated. Ranges can be dangerous
because of the possibility that the range could change and the case statement
may not be reexamined.
exception
It is acceptable to use ranges for possible values only when the user is
certain that new values will never be inserted among the old ones, as for
example, in the range of ASCII characters: 'a' .. 'z'.
note
Ranges that are needed in case statements can use constrained subtypes to
enhance maintainability. It is easier to maintain because the declaration of
the range can be placed where it is logically part of the abstraction, not
buried in a case statement in the executable code.
subtype Lower_Case is Character range 'a' .. 'z';
subtype Upper_Case is Character range 'A' .. 'Z';
subtype Control is Character range ASCII.Nul .. ASCII.Us;
subtype Numbers is Character range '0' .. '9';
...
case Input_Char is
when Lower_Case => Capitalize(Input_Char);
when Upper_Case => null;
when Control => raise Invalid_Input;
when Numbers => null;
...
end case;
5.6.4 Loops
guideline
o Use for loops whenever possible.
o Use while loops when the number of iterations cannot be calculated
before entering the loop, but a simple continuation condition can be applied
at the top of the loop.
o Use plain loops with exit statements for more complex situations.
o Avoid exit statements in while and for loops.
o Minimize the number of ways to exit a loop.
example
To iterate over all elements of an array:
for 1 in Array_Name'Range loop
...
end loop;
To iterate over all elements in a linked list:
Pointer := Head_Of_List;
while Pointer /= null loop
...
Pointer := Pointer.Next;
end loop;
Situations requiring a "loop and a half" arise often. For this use:
P_And_Q_Processing:
loop
P;
exit P_And_Q_Processing when Condition_Dependent_On_P;
Q;
end loop P_And_Q_Processing;
rather than:
P;
while not Condition_Dependent_On_P loop
Q;
P;
end loop;
rationale
A for loop is bounded, so it cannot be an "infinite loop." This is enforced by
the Ada language which requires a finite range in the loop specification and
which does not allow the loop counter of a for loop to be modified by a
statement executed within the loop. This yields a certainty of understanding
for the reader and the writer not associated with other forms of loops. A for
loop is also easier to maintain because the iteration range can be expressed
using attributes of the data structures upon which the loop operates, as shown
in the example above where the range changes automatically whenever the
declaration of the array is modified. For these reasons, it is best to use the
for loop whenever possible; that is, whenever simple expressions can be used
to describe the first and last values of the loop counter.
The while loop has become a very familiar construct to most programmers. At a
glance it indicates the condition under which the loop continues. Use the
while loop whenever it is not possible to use the for loop, but there is a
simple boolean expression describing the conditions under which the loop
should continue, as shown in the example above.
The plain loop statement should be used in more complex situations, even if it
is possible to contrive a solution using a for or while loop in conjunction
with extra flag variables or exit statements. The criteria in selecting a loop
construct is to be as clear and maintainable as possible. It is a bad idea to
use an exit statement from within a for or while loop because it is misleading
to the reader after having apparently described the complete set of loop
conditions at the top of the loop. A reader who encounters a plain loop
statement expects to see exit statements.
There are some familiar looping situations which are best achieved with the
plain loop statement. For example, the semantics of the Pascal repeat until
loop, where the loop is always executed at least once before the termination
test occurs, are best achieved by a plain loop with a single exit at the end
of the loop. Another common situation is the "loop and a half" construct,
shown in the example above, where a loop must terminate somewhere within the
sequence of statements of the body. Complicated "loop and a half" constructs
simulated with while loops often require the introduction of flag variables,
or duplication of code before and during the loop, as shown in the example.
Such contortions make the code more complex and less reliable.
Minimize the number of ways to exit a loop in order to make the loop more
understandable to the reader. It should be rare that you need more than two
exit paths from a loop. When you do, be sure to use exit statements for all of
them, rather than adding an exit statement to a for or while loop.
5.6.5 Exit Statements
guideline
o Use exit statements to enhance the readability of loop termination
code (NASA 1987).
o Use exit when ... rather than if ... then exit whenever possible (NASA
1987).
o Review exit statement placement.
example
See the examples in Guidelines and .
rationale
It is more readable to use exit statements than to try to add boolean flags to
a while loop condition to simulate exits from the middle of a loop. Even if
all exit statements would be clustered at the top of the loop body, the
separation of a complex condition into multiple exit statements can simplify
and make it more readable and clear. The sequential execution of two exit
statements is often more clear than the short-circuit control forms.
The exit when form is preferable to the if ... then, exit form because it
makes the word exit more visible by not nesting it inside of any control
construct. The if ... then exit form is needed only in the case where other
statements, in addition to the exit statement, must be executed conditionally.
For example:
if Status = Done then
Shut_Down;
return;
end if;
Loops with many scattered exit statements can indicate fuzzy thinking as
regards the loop's purpose in the algorithm. Such an algorithm might be coded
better some other way, e.g., with a series of loops. Some rework can often
reduce the number of exit statements and make the code clearer.
See also Guidelines and .
5.6.6 Recursion and Iteration Bounds
guideline
o Consider specifying bounds on loops.
o Consider specifying bounds on recursion.
example
Establishing an iteration bound:
Safety_Counter := 0;
Process_List:
loop
exit when Current_Item = null;
...
Current_Item := Current_Item.Next;
...
Safety_Counter := Safety_Counter + 1;
if Safety_Counter > 1_000_000 then
raise Safety_Error;
end if;
end loop Process_List;
Establishing a recursion bound:
procedure Depth_First (Root : in Tree;
Safety_Counter : in Recursion_Bound
:= Recursion_Bound'Last) is
begin
if Root /= null then
if Safety_Counter = 0 then
raise Recursion_Error;
end if;
Depth_First
(Root.Left_Branch, Safety_Counter - 1); -- recursivecall
Depth_First
(Root.Right_Branch, Safety_Counter - 1); -- recursivecall
... -- normal subprogram body
end if;
end Depth_First;
Following are examples of this subprogram's usage. One call specifies a
maximum recursion depth of 50. The second takes the default (one thousand).
The third uses a computed bound:
Depth_First(Root, 50);
Depth_First(Root);
Depth_First(Root, Current_Tree_Height);
rationale
Recursion, and iteration using structures other than for statements, can be
infinite because the expected terminating condition does not arise. Such
faults are sometimes quite subtle, may occur rarely, and may be difficult to
detect because an external manifestation might be absent or substantially
delayed.
By including counters and checks on the counter values, in addition to the
loops themselves, you can prevent many forms of infinite loops. The inclusion
of such checks is one aspect of the technique called Safe Programming
(Anderson and Witty 1978).
The bounds of these checks do not have to be exact, just realistic. Such
counters and checks are not part of the primary control structure of the
program but a benign addition functioning as an execution-time "safety net"
allowing error detection and possibly recovery from potential infinite loops
or infinite recursion.
note
If a loop uses the for iteration scheme (Guideline ), it follows this
guideline.
exceptions
Embedded control applications have loops that are intended to be infinite.
Only a few loops within such applications should qualify as exceptions to this
guideline. The exceptions should be deliberate (and documented) policy
decisions.
This guideline is most important to safety critical systems. For other
systems, it may be overkill.
5.6.7 Goto Statements
guideline
o Do not use goto statements.
rationale
A goto statement is an unstructured change in the control flow. Worse, the
label does not require an indicator of where the corresponding goto
statement(s) are. This makes code unreadable and makes its correct execution
suspect.
Other languages use goto statements to implement loop exits and exception
handling. Ada's support of these constructs makes the goto statement
extremely rare.
note
If you should ever use a goto statement, highlight both it and the label with
blank space. Indicate at the label where the corresponding goto statement(s)
may be found.
5.6.8 Return Statements
guideline
o Minimize the number of returns from a subprogram (NASA 1987).
o Highlight returns with comments or white space to keep them from being
lost in other code.
example
The following code fragment is longer and more complex than necessary:
if Pointer /= null then
if Pointer.Count > 0 then
return True;
else -- Pointer.Count = 0
return False;
end if;
else -- Pointer = null
return False;
end if;
It should be replaced with the shorter, more concise, and clearer equivalent
line:
return Pointer /= null and then Pointer.Count > 0;
rationale
Excessive use of returns can make code confusing and unreadable. Only use
return statements where warranted. Too many returns from a subprogram may be
an indicator of cluttered logic. If the application requires multiple returns,
use them at the same level (i.e., as in different branches of a case
statement), rather than scattered throughout the subprogram code. Some rework
can often reduce the number of return statements to one and make the code more
clear.
exception
Do not avoid return statements if it detracts from natural structure and code
readability.
5.6.9 Blocks
guideline
o Use blocks to localize the scope of declarations.
o Use blocks to perform local renaming.
o Use blocks to define local exception handlers.
example
with Motion;
with Accelerometer_Device;
...
---------------------------------------------------------------------
function Maximum_Velocity return Motion.Velocity is
Cumulative : Motion.Velocity := 0.0;
begin -- Maximum_Velocity
-- Initialize the needed devices
...
Calculate_Velocity_From_Sample_Data:
declare
Current : Motion.Acceleration := 0.0;
Accelerometer : Accelerometer_Device.Interface;
Time_Delta : Duration;
begin -- Calculate_Velocity_From_Sample_Data
for I in 1 .. Accelerometer_Device.Sample_Limit loop
Get_Samples_And_Ignore_Invalid_Data:
begin
Accelerometer.Value(Current, Time_Delta);
exception
when Numeric_Error | Constraint_Error =>
null; -- Continue trying
when Accelerometer_Device.Failure =>
raise Accelerometer_Device_Failed;
end Get_Samples_And_Ignore_Invalid_Data;
exit when Motion."<"(Current, 0.0); -- Slowing down
Update_Velocity:
declare
use Motion; -- for infix operators and exceptions;
begin
Cumulative := Cumulative + Current * Time_Delta;
exception
when Numeric_Error | Constraint_Error =>
raise Maximum_Velocity_Exceeded;
end Update_Velocity;
end loop;
end Calculate_Velocity_From_Sample_Data;
return Cumulative;
end Maximum_Velocity;
---------------------------------------------------------------------
...
rationale
Blocks break up large segments of code and isolate details relevant to each
subsection of code. Variables that are only used in a particular section of
code are clearly visible when a declarative block delineates that code.
Renaming may simplify the expression of algorithms and enhance readability for
a given section of code. But it is confusing when a rename clause is visually
separated from the code to which it applies. The declarative region allows
the renames to be immediately visible when the reader is examining code which
uses that abbreviation. Guideline 5.7.1 discusses a similar guideline
concerning the `use' clause.
Local exception handlers can catch exceptions close to the point of origin and
allow them to either be handled, propogated, or converted.
5.6.10 Aggregates
guideline
o Use an aggregate instead of a sequence of assignments to assign values
to all components of a record.
o Use an aggregate instead of a temporary variable when building a
record to pass as an actual parameter.
o Use positional association only when there is a conventional ordering
of the arguments.
example
It is better to use aggregates:
Set_Position((X, Y));
Employee_Record := (Number => 42,
Age => 51,
Department => Software_Engineering);
than to use consecutive assignments or temporary variables:
Temporary_Position.X := 100;
Temporary_Position.Y := 200;
Set_Position(Temporary_Position);
Employee_Record.Number := 42;
Employee_Record.Age := 51;
Employee_Record.Department := Software_Engineering;
rationale
Using aggregates during maintenance is beneficial. If a record structure is
altered, but the corresponding aggregate is not, the compiler flags the
missing field in the aggregate assignment. It would not be able to detect the
fact that a new assignment statement should have been added to a list of
assignment statements.
Aggregates can also be a real convenience in combining data items into a
record or array structure required for passing the information as a parameter.
Named component association makes aggregates more readable.
5.7 VISIBILITY
As noted in Guideline 4.2, Ada's ability to enforce information hiding and
separation of concerns through its visibility controlling features is one of
the most important advantages of the language. Subverting these features, for
example by over liberal use of the use clause, is wasteful and dangerous.
5.7.1 The Use Clause
guideline
o Minimize using the use clause (Nissen and Wallis 1984).
o Consider using the use clause in the following situations:
- Infix operators are needed
- Standard packages are needed and no ambiguous references are introduced
- References to enumeration literals are needed
o Consider the renames clause to avoid the use clause.
o Localize the effect of all use clauses.
example
This is a modification of the example from Guideline . The effect of a use
clause is localized.
...
procedure Compiler is
...
package Listing_Facilities is
end Listing_Facilities;
...
package body Listing_Facilities is separate;
...
end Compiler;
------------------------------------------------------------------------
with Text_IO;
separate (Compiler)
package body Listing_Facilities is
...
---------------------------------------------------------------------
procedure New_Line_Of_Print is
use Text_IO;
begin
...
end New_Line_Of_Print;
...
end Listing_Facilities;
rationale
These guidelines allow you to maintain a careful balance between
maintainability and readability. Excessive use of the use clause may indeed
make the code read more like prose text. However, the maintainer may also
need to resolve references and identify ambiguous operations. In the absence
of tools to resolve these references and identify the impact of changing use
clauses, fully qualified names are the best alternative.
Avoiding the use clause forces you to use fully qualified names. In large
systems, there may be many library units named in with clauses. When
corresponding use clauses accompany the with clauses and the simple names of
the library packages are omitted (as is allowed by the use clause), references
to external entities are obscured and identification of external dependencies
becomes difficult.
In some situations, the benefits of the use clause are clear. The use clause
can make several infix operators visible without the need for renames clauses.
A standard package can be used with the obvious assumption that the reader is
very familiar with those packages and that additional overloading will not be
introduced.
You can minimize the scope of the use clause by placing it in the body of a
package or subprogram or by encapsulating it in a block to restrict visibility.
notes
Avoiding the use clause completely can cause problems with enumeration
literals, which must then be fully qualified. This problem can be solved by
declaring constants with the enumeration literals as their values, except that
such constants cannot be overloaded like enumeration literals.
An argument defending the use clause can be found in Rosen (1987).
automation note
There are tools that can analyze your Ada source code, resolve overloading of
names, and automatically convert between the use clause or fully qualified
names.
5.7.2 The Renames Clause
guideline
o Rename a long, fully qualified name to reduce the complexity if it
becomes unwieldy (Guideline ).
o Rename declarations for visibility purposes rather than using the use
clause, especially for infix operators (Guideline ).
o Rename parts when interfacing to reusable components originally
written with nondescriptive or inapplicable nomenclature.
o Use a project-wide standard list of abbreviations to rename common
packages.
example
procedure Disk_Write (Track_Name : in Track;
Item : in Data) renames
System_Specific.Device_Drivers.Disk_Head_Scheduler.Transmit;
rationale
If the renaming facility is abused, the code can be difficult to read. A
renames clause can substitute an abbreviation for a qualifier or long package
name locally. This can make code more readable yet anchor the code to the full
name. However, the use of renames clauses can often be avoided or made
obviously undesirable by carefully choosing names so that fully qualified
names read well. The list of renaming declarations serves as a list of
abbreviation definitions (see Guideline ). By renaming imported infix
operators, the use clause can often be avoided. The method prescribed in the
Ada Language Reference Manual (Department of Defense 1983) for renaming a type
is to use a subtype (see Guideline ). Often the parts recalled from a reuse
library do not have names that are as general as they could be or that match
the new application's naming scheme. An interface package exporting the
renamed subprograms can map to your project's nomenclature.
5.7.3 Overloaded Subprograms
guideline
o Limit overloading to widely used subprograms that perform similar
actions on arguments of different types (Nissen and Wallis 1984).
example
function Sin (Angles : in Matrix_Of_Radians) return Matrix;
function Sin (Angles : in Vector_Of_Radians) return Vector;
function Sin (Angle : in Radians) return Small_Real;
function Sin (Angle : in Degrees) return Small_Real;
rationale
Excessive overloading can be confusing to maintainers (Nissen and Wallis 1984,
65). There is also the danger of hiding declarations if overloading becomes
habitual. Attempts to overload an operation may actually hide the original
operation if the parameter profile is not distinct. From that point on, it is
not clear whether invoking the new operation is what the programmer intended
or whether the programmer intended to invoke the hidden operation and
accidentally hid it.
note
This guideline does not prohibit subprograms with identical names declared in
different packages.
5.7.4 Overloaded Operators
guideline
o Preserve the conventional meaning of overloaded operators (Nissen and
Wallis 1984).
o Use "+" to identify adding, joining, increasing, and enhancing kinds of
functions.
o Use "-" to identify subtraction, separation, decreasing, and depleting
kinds of functions.
example
function "+" (X : in Matrix;
Y : in Matrix)
return Matrix;
...
Sum := A + B;
rationale
Subverting the conventional interpretation of operators leads to confusing
code.
note
There are potential problems with any overloading. For example, if there are
several versions of the "+" operator, and a change to one of them affects the
number or order of its parameters, locating the occurrences that must be
changed can be difficult.
5.7.5 Overloading the Equality Operator
guideline
o Do not depend on the definition of equality provided by private types.
o When overloading the equality operator for limited private types,
maintain the properties of an algebraic equivalence relation.
rationale
The predefined equality operation provided with private types is dependent on
the data structure chosen to implement that type. If access types are used,
then equality will mean the operands have the same pointer value. If discrete
types are used, then equality will mean the operands have the same value. If
a floating point type is used, then equality is based on Ada model intervals
(see Guideline .).
Any assumptions about the meaning of equality for private types will create a
dependency on the implementation of that type. See Gonzalez (1992) for a
detailed discussion.
For limited private types, the definition of "=" is optional. When provided,
however, there is a conventional algebraic meaning implied by this symbol. As
described in Baker (1991), the following properties should remain true for the
equality operator:
- reflexive: a = a
- symmetric: a = b ==> b = a
- transitive: a = b and b = c ==> a = c
5.8 USING EXCEPTIONS
Ada exceptions are a reliability-enhancing language feature designed to help
specify program behavior in the presence of errors or unexpected events.
Exceptions are not intended to provide a general purpose control construct.
Further, liberal use of exceptions should not be considered sufficient for
providing full software fault tolerance (Melliar-Smith and Randall 1987).
This section addresses the issues of how and when to avoid raising exceptions,
how and where to handle them, and whether to propagate them. Information on
how to use exceptions as part of the interface to a unit include what
exceptions to declare and raise and under what conditions to raise them.
Other issues are addressed in Guidelines and .
5.8.1 Handling Versus Avoiding Exceptions
guideline
o Avoid causing exceptions to be raised when it is easy and efficient to
do so.
o Provide handlers for exceptions which cannot be avoided.
o Use exception handlers to enhance readability by separating fault
handling from normal execution.
o Do not use exceptions and exception handlers as goto statements.
rationale
In many cases, it is possible to detect easily and efficiently that an
operation you are about to perform would raise an exception. In such a case,
it is a good idea to check rather than allowing the exception to be raised and
handling it with an exception handler. For example, check each pointer for
NULL when traversing a linked list of records connected by pointers. Also,
test an integer for zero before dividing by it, and call an interrogative
function Stack_Is_Empty before invoking the POP procedure of a stack package.
Such tests are appropriate when they can be performed easily and efficiently,
as a natural part of the algorithm being implemented.
However, error detection in advance is not always so simple. There are cases
where such a test is too expensive or too unreliable. In such cases, it is
better to attempt the operation within the scope of an exception handler so
that the exception is handled if it is raised. For example, in the case of a
linked list implementation of a list, it is very inefficient to call a
function Entry_Exists before each call to the procedure Modify_Entry simply to
avoid raising the exception Entry_Not_Found. It takes as much time to search
the list to avoid the exception as it takes to search the list to perform the
update. Similarly, it is much easier to attempt a division by a real number
within the scope of an exception handler to handle numeric overflow than to
test in advance whether the dividend is too large or the divisor too small for
the quotient to be representable on the machine.
In concurrent situations, tests done in advance can also be unreliable. For
example, if you want to modify an existing file on a multi-user system, it is
safer to attempt to do so within the scope of an exception handler than to
test in advance whether the file exists, whether it is protected, whether
there is room in the file system for the file to be enlarged, etc. Even if you
tested for all possible errors conditions, there is no guarantee that nothing
would change after the test and before the modification operation. You still
need the exception handlers, so the advance testing serves no purpose.
Whenever such a case does not apply, normal and predictable events should be
handled by the code without the abnormal transfer of control represented by an
exception. When fault handling and only fault handling code is included in
exception handlers, the separation makes the code easier to read. The reader
can skip all the exception handlers and still understand the normal flow of
control of the code. For this reason, exceptions should never be raised and
handled within the same unit, as a form of a goto statement to exit from a
loop, if, case, or block statement.
5.8.2 Handlers for others
guideline
o Use caution when programming handlers for others.
o Provide a handler for others in suitable frames to protect against
unexpected exceptions being propagated without bound, especially in safety
critical systems.
o Use others only to catch exceptions you cannot enumerate explicitly,
preferably only to flag a potential abort.
o Avoid using others during development.
rationale
Providing a handler for others allows you to follow the other guidelines in
this section. It affords a place to catch and convert truly unexpected
exceptions that were not caught by the explicit handlers. While it may be
possible to provide "fire walls" against unexpected exceptions being
propagated without providing handlers in every block, you can convert the
unexpected exceptions as soon as they arise. The others handler cannot
discriminate between different exceptions, and, as a result, any such handler
must treat the exception as a disaster. Even such a disaster can still be
converted into a user-defined exception at that point. Since a handler for
others catches any exception not otherwise handled explicitly, one placed in
the frame of a task or of the main subprogram affords the opportunity to
perform final clean-up and to shut down cleanly.
Programming a handler for others requires caution because it cannot
discriminate either which exception was actually raised or precisely where it
was raised. Thus, the handler cannot make any assumptions about what can be or
even what needs to be "fixed."
The use of handlers for others during development, when exception occurrences
can be expected to be frequent, can hinder debugging. It is much more
informative to the developer to see a traceback with the actual exception
listed than the converted exception. Furthermore, many tracebacks do not list
the point where the original exception was raised if it was caught by a
handler.
note
The arguments in the preceding paragraph apply only to development time, when
traceback listings are useful. They are not useful to users and can be
dangerous. The handler should be included in comment form at the outset of
development and the double dash removed before delivery.
5.8.3 Propagation
guideline
o Handle all exceptions, both user and predefined.
o For every exception that might be raised, provide a handler in
suitable frames to protect against undesired propagation outside the
abstraction.
rationale
The statement that "it can never happen" is not an acceptable programming
approach. You must assume it can happen and be in control when it does. You
should provide defensive code routines for the "cannot get here" conditions.
Some existing advice calls for catching and propagating any exception to the
calling unit. This advice can stop a program. You should catch the exception
and propagate it, or a substitute, only if your handler is at the wrong
abstraction level to effect recovery. Effecting recovery can be difficult, but
the alternative is a program that does not meet its specification.
Making an explicit request for termination implies that your code is in
control of the situation and has determined that to be the only safe course of
action. Being in control affords opportunities to shut down in a controlled
manner (clean up loose ends, close files, release surfaces to manual control,
sound alarms), and implies that all available programmed attempts at recovery
have been made.
5.8.4 Localizing the Cause of an Exception
guideline
o Do not rely on being able to identify the fault raising predefined or
implementation-defined exceptions.
o Use blocks to associate localized sections of code with their own
exception handlers.
example
See Guideline .
rationale
It is very difficult to determine in an exception handler exactly which
statement and which operation within that statement raised an exception,
particularly the predefined and implementation-defined exceptions. The
predefined and implementation-defined exceptions are candidates for conversion
and propagation to higher abstraction levels for handling there. User-defined
exceptions, being more closely associated with the application, are better
candidates for recovery within handlers.
User-defined exceptions can also be difficult to localize. Associating
handlers with small blocks of code helps to narrow the possibilities, making
it easier to program recovery actions. The placement of handlers in small
blocks within a subprogram or task body also allows resumption of the
subprogram or task after the recovery actions. If you do not handle exceptions
within blocks, the only action available to the handlers is to shut down the
task or subprogram as prescribed in Guideline .
note
The optimal size for the sections of code you choose to protect by a block and
its exception handlers is very application-dependent. Too small a granularity
forces you to expend much more effort in programming for abnormal actions than
for the normal algorithm. Too large a granularity reintroduces the problems of
determining what went wrong and of resuming normal flow.
5.9 ERRONEOUS EXECUTION
An Ada program is erroneous when it violates or extends the rules of the
language governing program behavior. Neither compilers nor run-time
environments are able to detect erroneous behavior in all circumstances and
contexts. As stated in Section 1.6 of Department of Defense (1983), "The
effects of erroneous execution are unpredictable." If the compiler does
detect an instance of an erroneous program, its options are to indicate a
compile time error, to insert the code to raise Program_Error, possibly to
write a message to that effect, or to do nothing at all.
Erroneousness is not a concept unique to Ada. The guidelines below describe or
explain the specific instances of erroneousness defined in the Ada Language
Reference Manual. Although Incorrect Order Dependencies is not, strictly
speaking, a case of erroneous execution, the rationale for avoiding such
dependencies is the same. Consequently, the guideline is included in this
section.
5.9.1 Unchecked Conversion
guideline
o Use Unchecked_Conversion only with the utmost care (Department of
Defense 1983, w13.10.2).
o Ensure the value resulting from Unchecked_Conversion is in range.
o Isolate the use of Unchecked_Conversion in package bodies.
example
The following example may run without exception, depending on the
implementation:
------------------------------------------------------------------------
with Unchecked_Conversion;
with Text_IO;
procedure Test is
type Color is (Red, Yellow, Blue);
function Integer_To_Color is
new Unchecked_Conversion (Source => Integer,
Target => Color);
A_Color : Color;
List : array (Color) of Boolean;
Data : Boolean;
begin -- Test
A_Color := Integer_To_Color(15);
Data := List(A_Color);
Text_IO.Put_Line(Color'Image(A_Color));
end Test;
------------------------------------------------------------------------
rationale
An unchecked conversion is a bit-for-bit copy without regard to the meanings
attached to those bits and bit positions by either the source or the
destination type. The source bit pattern can easily be meaningless in the
context of the destination type. Unchecked conversions can create values that
violate type constraints on subsequent operations. Unchecked conversion of
objects mismatched in size has implementation-dependent results.
5.9.2 Unchecked Deallocation
guideline
o Isolate the use of Unchecked_Deallocation in package bodies.
rationale
Most of the reasons for using Unchecked_Deallocation with caution have been
given in Guideline . When this feature is used, there is no checking that
there is only one access path to the storage being deallocated. Thus, any
other access paths are not made null. Depending on such a check is erroneous.
5.9.3 Dependence on Parameter Passing Mechanism
guideline
o Do not write code whose correct execution depends on the particular
parameter passing mechanism used by an implementation (Department of Defense
1983 and Cohen 1986).
example
The output of this program depends on the particular parameter passing
mechanism that was used:
------------------------------------------------------------------------
with Text_IO;
procedure Outer is
type Coordinates is
record
X : Integer := 0;
Y : Integer := 0;
end record;
Outer_Point : Coordinates;
package Integer_IO is
new Text_IO.Integer_IO (Num => Integer);
---------------------------------------------------------------------
procedure Inner (Inner_Point : in out Coordinates) is
begin
Inner_Point.X := 5;
-- The following line causes the output of the program to
-- depend on the parameter passing mechanism.
Integer_IO.Put(Outer_Point.X);
end Inner;
---------------------------------------------------------------------
begin -- Outer
Integer_IO.Put(Outer_Point.X);
Inner (Outer_Point);
Integer_IO.Put(Outer_Point.X);
end Outer;
------------------------------------------------------------------------
If the parameter passing mechanism is by copy, the results on the standard
output file are:
0 0 5
If the parameter passing mechanism is by reference, the results are:
0 5 5
rationale
The language definition specifies that a parameter whose type is an array,
record, or task type can be passed by copy or reference. It is erroneous to
assume that either mechanism is used in a particular case.
exceptions
Frequently, when interfacing Ada to foreign code, dependence on parameter
passing mechanisms used by a particular implementation is unavoidable. In this
case, isolate the calls to the foreign code in an interface package that
exports operations that do not depend on the parameter-passing mechanism.
5.9.4 Multiple Address Clauses
guideline
o Use address clauses to map variables and entries to the hardware
device or memory, not to model the FORTRAN "equivalence" feature.
example
Single_Address : constant ...
Interrupt_Vector_Table : Hardware_Array;
for Interrupt_Vector_Table use at Single_Address;
rationale
The result of specifying a single address for multiple objects or program
units is undefined, as is specifying multiple addresses for a single object or
program unit. Specifying multiple address clauses for an interrupt entry is
also undefined. It does not necessarily overlay objects or program units, or
associate a single entry with more than one interrupt.
5.9.5 Suppression of Exception Check
guideline
o Do not suppress exception checks during development.
o Minimize suppression of exception checks during operation.
S If necessary, introduce blocks that encompass the smallest range of
statements that can safely have exception checking removed.
rationale
If you disable exception checks and program execution results in a condition
in which an exception would otherwise occur, the program execution is
erroneous. The results are unpredictable. Further, you must still be prepared
to deal with the suppressed exceptions if they are raised in and propagated
from the bodies of subprograms, tasks, and packages you call.
By minimizing the code which has exception checking removed, you increase the
reliability of the program. There is a rule of thumb which suggests that 20
percent of the code is responsible for 80 percent of the CPU time. So once
you have identified the code that actually needs exception checking removed,
it is wise to isolate it in a block (with appropriate comments) and leave the
surrounding code with exception checking in effect.
5.9.6 Initialization
guideline
o Initialize all objects prior to use.
o Ensure elaboration of an entity before using it.
o Use function calls in declarations cautiously.
example
------------------------------------------------------------------------
package Robot_Controller is
...
function Sense return Position;
...
end Robot_Controller;
------------------------------------------------------------------------
package body Robot_Controller is
...
Goal : Position := Sense; -- This raises Program_Error
...
---------------------------------------------------------------------
function Sense return Position is
begin
...
end Sense;
---------------------------------------------------------------------
begin -- Robot_Controller
Goal := Sense; -- The function has been elaborated.
...
end Robot_Controller;
------------------------------------------------------------------------
rationale
Ada does not define an initial default value for objects of any type other
than access types. Using the value of an object before it has been assigned a
value causes unpredictable behavior, possibly raising an exception. Objects
can be initialized implicitly by declaration or explicitly by assignment
statements. Initialization at the point of declaration is safest as well as
easiest for maintainers. You can also specify default values for components of
records as part of the type declarations for those records.
Ensuring initialization does not imply initialization at the declaration. In
the example above, Goal must be initialized via a function call. This cannot
occur at the declaration, because the function Sense has not yet been
elaborated, but can occur later as part of the sequence of statements of the
body of the enclosing package.
An unelaborated function called within a declaration (initialization) raises
the exception, Program_Error, that must be handled outside of the unit
containing the declarations. This is true for any exception the function
raises even if it has been elaborated.
If an exception is raised by a function call in a declaration, it is not
handled in that immediate scope. It is raised to the enclosing scope. This can
be controlled by nesting blocks.
note
Sometimes, elaboration order can be dictated with pragma Elaborate. Pragma
Elaborate only applies to library units.
5.9.7 Direct_IO and Sequential_IO
guideline
o Ensure that values obtained from Direct_IO and Sequential_IO are in
range.
rationale
As with Unchecked_Conversion, there is no check on the value obtained from the
read operations found in Direct_IO and Sequential_IO. See Guideline 5.9.1 for
an example.
note
It is sometimes difficult to force an optimizing compiler to perform the
necessary checks on a value that the compiler believes is in range. Most
compiler vendors allow the option of suppressing optimization which can be
helpful.
5.9.8 Incorrect Order Dependencies
guideline
o Avoid depending on the order in which certain constructs in Ada are
evaluated (see Department of Defense 1983, I-17).
rationale
As stated in the Ada Language Reference Manual, an incorrect order dependency
may arise whenever ". . . different parts of a given construct are to be
executed in some order that is not defined by the language. . . . The
construct is incorrect if execution of these parts in a different order would
have a different effect." (Department of Defense 1983, w1.6).
While an incorrect order dependency may not adversely affect the program on a
certain implementation, the code might not execute correctly when it is
ported. Avoid incorrect order dependencies, but also recognize that even an
unintentional error of this kind could prohibit portability.
5.10 SUMMARY
optional parts of the syntax
o Associate names with loops when they are nested.
o Associate names with blocks when they are nested.
o Use loop names on all exit statements from nested loops.
o Include the simple name at the end of a package specification and body.
o Include the simple name at the end of a task specification and body.
o Include the simple name at the end of an accept statement.
o Include the designator at the end of a subprogram body.
parameter lists
o Name formal parameters descriptively to reduce the need for comments.
o Use named parameter association in calls of infrequently used
subprograms or entries with many formal parameters.
o Use named association when instantiating generics.
o Use named association for clarification when the actual parameter is
any literal or expression.
o Use named association when supplying a nondefault value to an optional
parameter.
o Provide default parameters to allow for occasional, special use of
widely used subprograms or entries.
o Place default parameters at the end of the formal parameter list.
o Consider providing default values to new parameters added to an
existing subprogram.
o Show the mode indication of all procedure and entry parameters.
o Use in out only when the parameter is both read from and updated.
types
o Use existing types as building blocks by deriving new types from them.
o Use range constraints on subtypes.
o Define new types, especially derived types, to include the largest set
of possible values, including boundary values.
o Constrain the ranges of derived types with subtypes, excluding
boundary values.
o Avoid anonymous types.
o Use anonymous types for array variables when no suitable type exists
and the array will not be referenced as a whole.
o Use limited private types in preference to private types.
o Use private types in preference to nonprivate types.
o Explicitly export needed operations rather than easing restrictions.
data structures
o Use records to group heterogeneous but related data.
o Consider records to map to I/O device data.
o Record structures should not always be flat. Factor out common parts.
o For a large record structure, group related components into smaller
subrecords.
o For nested records, pick element names that read well when inner
elements are referenced.
o Differentiate between static and dynamic data. Use dynamically
allocated objects with caution.
o Use dynamically allocated data structures only when it is necessary to
create and destroy them dynamically or to be able to reference them by
different names.
o Do not drop pointers to undeallocated objects.
o Do not leave dangling references to deallocated objects.
o Initialize all access variables and components within a record.
o Do not rely on memory deallocation.
o Deallocate explicitly.
o Use length clauses to specify total allocation size.
o Provide handlers for Storage_Error.
expressions
o Use 'First or 'Last instead of numeric literals to represent the first
or last values of a range.
o Use the type or subtype name of the range instead of 'First .. 'Last.
o Use array attributes 'First, 'Last, or 'Length instead of numeric
literals for accessing arrays.
o Use the 'Range of the array instead of the name of the index subtype
to express a range.
o Use 'Range instead of 'first .. 'Last to express a range.
o Use parentheses to specify the order of subexpression evaluation to
clarify expressions.
o Use parentheses to specify the order of evaluation for subexpressions
whose correctness depends on left to right evaluation.
o Avoid names and constructs that rely on the use of negatives.
o Choose names of flags so they represent states that can be used in
positive form.
o Use short-circuit forms of the logical operators.
o Use <= and >= in relational expressions with real operands instead of
=.
statements
o Minimize the depth of nested expressions.
o Minimize the depth of nested control structures.
o Try simplification heuristics.
o Use slices rather than a loop to copy part of an array.
o Never use an others choice in a case statement.
o Do not use ranges of enumeration literals in case statements.
o Use for loops whenever possible.
o Use while loops when the number of iterations cannot be calculated
before entering the loop, but a simple continuation condition can be applied
at the top of the loop.
o Use plain loops with exit statements for more complex situations.
o Avoid exit statements in while and for loops.
o Minimize the number of ways to exit a loop.
o Use exit statements to enhance the readability of loop termination
code.
o Use exit when ... rather than if ... then exit whenever possible.
o Review exit statement placement.
o Consider specifying bounds on loops.
o Consider specifying bounds on recursion.
o Do not use goto statements.
o Minimize the number of returns from a subprogram.
o Highlight returns with comments or white space to keep them from being
lost in other code.
o Use blocks to localize the scope of declarations.
o Use blocks to perform local renaming.
o Use blocks to define local exception handlers.
o Use an aggregate instead of a sequence of assignments to assign values
to all components of a record.
o Use an aggregate instead of a temporary variable when building a
record to pass as an actual parameter.
o Use positional association only when there is a conventional ordering
of the arguments.
visibility
o Minimize using the use clause.
o Consider using the use clause in the following situations:
- Infix operators are needed
- Standard packages are needed and no ambiguous references are introduced
- References to enumeration literals are needed
o Consider the renames clause to avoid the use clause.
o Localize the effect of all use clauses.
o Rename a long, fully qualified name to reduce the complexity if it
becomes unwieldy (Guideline ).
o Rename declarations for visibility purposes rather than using the use
clause, especially for infix operators (Guideline ).
o Rename parts when interfacing to reusable components originally
written with nondescriptive or inapplicable nomenclature.
o Use a project-wide standard list of abbreviations to rename common
packages.
o Limit overloading to widely used subprograms that perform similar
actions on arguments of different types.
o Preserve the conventional meaning of overloaded operators.
o Use "+" to identify adding, joining, increasing, and enhancing kinds of
functions.
o Use "-" to identify subtraction, separation, decreasing, and depleting
kinds of functions.
o Do not depend on the definition of equality provided by private types.
o When overloading the equality operator for limited private types,
maintain the properties of an algebraic equivalence relation.
using exceptions
o Avoid causing exceptions to be raised when it is easy and efficient to
do so.
o Provide handlers for exceptions which cannot be avoided.
o Use exception handlers to enhance readability by separating fault
handling from normal execution.
o Do not use exceptions and exception handlers as goto statements.
o Use caution when programming handlers for others.
o Provide a handler for others in suitable frames to protect against
unexpected exceptions being propagated without bound, especially in safety
critical systems.
o Use others only to catch exceptions you cannot enumerate explicitly,
preferably only to flag a potential abort.
o Avoid using others during development.
o Handle all exceptions, both user and predefined.
o For every exception that might be raised, provide a handler in
suitable frames to protect against undesired propagation outside the
abstraction.
o Do not rely on being able to identify the fault raising predefined or
implementation-defined exceptions.
o Use blocks to associate localized sections of code with their own
exception handlers.
erroneous execution
o Use Unchecked_Conversion only with the utmost care.
o Ensure the value resulting from Unchecked_Conversion is in range.
o Isolate the use of UncheckeD_Conversion in package bodies.
o Isolate the use of Unchecked_Deallocation in package bodies.
o Do not write code whose correct execution depends on the particular
parameter passing mechanism used by an implementation.
o Use address clauses to map variables and entries to the hardware
device or memory, not to model the FORTRAN "equivalence" feature.
o Do not suppress exception checks during development.
o Minimize suppression of exception checks during operation.
o If necessary, introduce blocks that encompass the smallest range of
statements that can safely have exception checking removed.
o Initialize all objects prior to use.
o Ensure elaboration of an entity before using it.
o Use function calls in declarations cautiously.
o Ensure that values obtained from Direct_IO and Sequential_IO are in
range.
o Avoid depending on the order in which certain constructs in Ada are
evaluated.
CHAPTER 6
Concurrency
Concurrency exists as either apparent concurrency or real concurrency. In a
single processor environment apparent concurrency is the result of interleaved
execution of concurrent activities. In a multi-processor environment real
concurrency is the result of overlapped execution of concurrent activities.
Concurrent programming is more difficult and error prone than sequential
programming. The concurrent programming features of Ada are designed to make
it easier to write and maintain concurrent programs which behave consistently
and predictably, and avoid such problems as deadlock and starvation. The
language features themselves cannot guarantee that programs have these
desirable properties. They must be used with discipline and care, a process
supported by the guidelines in this chapter.
The correct usage of Ada concurrency features results in reliable, reusable,
and portable software. For example, using tasks to model concurrent activities
and using the rendezvous for the required synchronization between cooperating
concurrent tasks. Misuse of language features results in software that is
unverifiable and difficult to reuse or port. For example, using task
priorities or delays to manage synchronization is not portable. It is also
important that a reusable component not make assumptions about the order or
speed of task execution (i.e., about the compilers tasking implementation).
Avoid assuming that the rules of good sequential program design can be
applied, by analogy, to concurrent programs. For example, while multiple
returns from subprograms should be discouraged (Guideline ), multiple task
exits or termination points are often necessary and desirable.
6.1 TASKING
Many problems map naturally to a concurrent programming solution. By
understanding and correctly using the Ada language tasking features, you can
produce solutions that are independent of target implementation. Tasks provide
a means, within the Ada language, of expressing concurrent asynchronous
threads of control and relieving programmers from the problem of explicitly
controlling multiple concurrent activities.
Tasks cooperate to perform the required activities of the software.
Synchronization is required between individual tasks. The Ada rendezvous
provides a powerful mechanism for this synchronization.
6.1.1 Tasks
guideline
o Use tasks to model asynchronous entities within the problem domain.
o Use tasks to control or synchronize access to tasks or devices.
o Use tasks to define concurrent algorithms.
example
Asynchronous entities are the naturally concurrent objects within the problem
domain. These tend to be objects in the problem space that have state, such as
elevators in an elevator control system or satellites in a global positioning
system. The following is an example for an elevator control system:
------------------------------------------------------------------------
package Elevator_Objects is
...
type Elevator_States is (Moving, Idle, Stopped, At_Floor);
type Up_Down is (Up, Down);
---------------------------------------------------------------------
task type Elevators is
entry Initialize;
entry Close_Door;
entry Open_Door;
entry Stop;
entry Idle;
entry Start (Direction : in Up_Down);
entry Current_State (My_State : out Elevator_States;
Current_Location : out Float);
end Elevators;
---------------------------------------------------------------------
...
end Elevator_Objects;
------------------------------------------------------------------------
A task that manages updates from multiple concurrent user tasks to a graphic
display is an example of a control and synchronization task.
Multiple tasks that implement the decomposition of a large matrix
multiplication algorithm is an example of an opportunity for real concurrency
in a multi-processor target environment. In a single processor target
environment this approach may not be justified.
A task that updates a radar display every 30 milliseconds is an example of a
cyclic activity supported by a task.
A task that detects an over-temperature condition in a nuclear reactor and
performs an emergency shutdown of the systems is an example of a task to
support a high priority activity.
rationale
These guidelines reflect the intended uses of tasks. They all revolve around
the fact that a task has its own thread of control separate from the main
subprogram. The conceptual model for a task is a separate program with its own
virtual processor. This provides the opportunity to model entities from the
problem domain in terms more closely resembling those entities, and the
opportunity to handle physical devices as a separate concern from the main
algorithm of the application. Tasks also allow naturally concurrent activities
which can be mapped to multiple processors when available.
Resources shared between multiple tasks, such as devices and abstract data
structures, require control and synchronization since their operations are
not atomic. Drawing a circle on a display may require that many low level
operations be performed without interruption by another task. A display
manager would ensure that no other task accesses the display until all these
operations are complete.
6.1.2 Anonymous Task Types
guideline
o Use anonymous task types for single instances.
example
The example below illustrates the syntactic differences between the kinds of
tasks discussed here. Buffer is static and has a name, but its type is
anonymous. Because it is declared explicitly, the task type Buffer_Manager is
not anonymous. Channel is static and has a name, and its type is not
anonymous. Like all dynamic objects, Encrypted_Packet_Queue.all is
essentially anonymous, but its type is not.
task Buffer;
task type Buffer_Manager;
type Replaceable_Buffer is access Buffer_Manager;
...
Encrypted_Packet_Queue : Replaceable_Buffer;
Channel : Buffer_Manager;
...
Encrypted_Packet_Queue := new Buffer_Manager;
...
rationale
The use of named tasks of anonymous type avoids a proliferation of task types
that are only used once, and the practice communicates to maintainers that
there are no other task objects of that type. If the need arises later to have
additional tasks of the same type, then the work required to convert a named
task to a task type is minimal.
The consistent and logical use of task types, when necessary, contributes to
understandability. Identical tasks can be derived from a common task type.
Dynamically allocated task structures are necessary when you must create and
destroy tasks dynamically or when you must reference them by different names.
note
Though changing the task from an anonymous type to a task type is trivial,
structural changes to the software architecture may not be trivial.
Introduction of multiple tasks of the task type may require the scope of the
task type to change and may change the behavior of the network of
synchronizing tasks.
6.1.3 Dynamic Tasks
guideline
o Justify the use of dynamically allocated task objects.
o Avoid disassociating a dynamic task from all names.
example
The approach used in the following example below is not recommended. The
example shows why caution is required with dynamically allocated task objects.
It illustrates how a dynamic task can be disassociated from its name.
task type Radar_Track;
type Radar_Track_Pointer is access Radar_Track;
Current_Track : Radar_Track_Pointer;
---------------------------------------------------------------------
task body Radar_Track is
begin
loop
-- update tracking information
...
-- exit when out of range
delay 1.0;
end loop;
...
end Radar_Track;
---------------------------------------------------------------------
...
loop
...
-- Unless some code deals with non-null values of Current_Track,
-- (such as an array of existing tasks)
-- this assignment leaves the existing Radar_Track task running with
-- no way to signal it to abort or to instruct the system to
-- reclaim its resources.
Current_Track := new Radar_Track;
...
end loop;
rationale
A dynamically allocated task object is a task object created by the execution
of an allocator. Allocators can be used to avoid limiting the number of
tasks. Memory and timing requirements are positively or negatively affected
by the decision to use dynamic tasks. Both creation and deletion of dynamic
tasks and scheduling of dormant static tasks adversely affect performance.
Dormant static tasks incur memory overhead that can be avoided using dynamic
tasks. Creation and deletion of dynamic tasks is typically more expensive
than scheduling overhead in terms of CPU time.
Allocated task objects referenced by access variables allow you to generate
aliases; multiple references to the same object. Anomalous behavior can arise
when you reference an aborted task by another name.
A dynamically allocated task that is not associated with a name (a "dropped
pointer") cannot be referenced for the purpose of making entry calls, nor can
it be the direct target of an abort statement (see Guideline ).
6.1.4 Priorities
guideline
o Do not rely on pragma Priority to prioritize the service of entries.
o Arrange task bodies in order of their priorities (if possible).
example
For example, let the tasks have the following priorities:
task T1 ... pragma Priority (High) ... Server.Operation ...
task T2 ... pragma Priority (Medium) ... Server.Operation ...
task Server ... accept Operation ...
At some point in its execution, T1 is blocked. Otherwise, T2 and Server may
never execute. If T1 is blocked, it is possible for T2 to reach its call to
Server's entry (Operation) before T1. Suppose this has happened and that T1
now makes its entry call before Server has a chance to accept T2's call.
This is the timeline of events so far:
T1 blocks
T2 calls Server.Operation
T1 unblocks
T1 calls Server.Operation
Does Server accept the call from T1 or from T2?
Some people might expect that, due to its higher priority, T1's call would be
accepted by Server before that of T2. However, entry calls are queued in
first-in-first-out (FIFO) order and not queued in order of priority.
Therefore, the synchronization between T1 and Server is not affected by T1's
priority. As a result, the call from T2 is accepted first. This is a form of
priority inversion.
A solution might be to provide an entry for a High priority user and an entry
for a Medium priority user.
---------------------------------------------------------------------
task Server is
entry Operation_High_Priority;
entry Operation_Medium_Priority;
...
end Server;
---------------------------------------------------------------------
task body Server is
begin
loop
select
accept Operation_High_Priority do
Operation;
end Operation_High_Priority;
else -- accept any priority
select
accept Operation_High_Priority do
Operation;
end Operation_High_Priority;
or
accept Operation_Medium_Priority do
Operation;
end Operation_Medium_Priority;
or
terminate;
end select;
end select;
end loop;
...
end Server;
---------------------------------------------------------------------
However, in this approach T1 still waits for one execution of Operation when
T2 has already gained control of the task Server. In addition, the approach
increases the communication complexity (see Guideline ).
rationale
The pragma Priority allows relative priorities to be placed on tasks to
accomplish scheduling. Precision becomes a critical issue with hard-deadline
scheduling. However, there are certain problems associated with using
priorities that warrant caution.
Priority inversion occurs when lower priority tasks are given service while
higher priority tasks remain blocked. In the above example, this occurred
because entry queues are serviced in FIFO order, not by priority. There is
another situation referred to as a race condition. A program like the one in
the first example might often behave as expected as long as T1 calls
Server.Operation only when T2 is not already using Server.Operation or
waiting. You cannot rely on T1 always winning the race, since that behavior
would be due more to fate than to the programmed priorities. Race conditions
change when either adding code to an unrelated task or porting this code to a
new target. Task priorities are not a means of achieving mutual exclusion.
Arranging task bodies in order of priority will elaborate the higher priority
tasks first.
exceptions
When there are dependencies between tasks, the dependencies will influence the
order in which the tasks should be elaborated. In these cases, the
dependencies in conjunction with the task priorities should be used to order
the task bodies.
note
Work is being done to minimize these problems, including the introduction of a
scheduling algorithm known as the priority ceiling protocol (Goodenough and
Sha 1988). The priority ceiling protocol reduces the blocking time that causes
priority inversion to only one critical region (defined by the entries in a
task). The protocol also eliminates deadlock unless a task recursively tries
to access a critical region. This protocol is based on priority inheritance
and thus deviates from the standard Ada tasking paradigm.
Priorities are used to control when tasks run relative to one another. When
both tasks are not blocked waiting at an entry, the highest priority task is
given precedence. However, the most critical tasks in an application do not
always have the highest priority. For example, support tasks or tasks with
small periods may have higher priorities, because they need to run frequently.
6.1.5 Delay Statements
guideline
o Do not depend on a particular delay being achievable (Nissen and
Wallis 1984).
o Do not use a busy waiting loop instead of a delay.
o Design to limit polling to those cases where absolutely necessary.
o Do not use knowledge of the execution pattern of tasks to achieve
timing requirements.
example
The phase of a periodic task is the fraction of a complete cycle elapsed as
measured from a specified reference point. In the following example an
inaccurate delay causes the phase of the periodic task to drift over time
(i.e., the task starts later and later in the cycle):
Periodic:
loop
delay Interval;
...
end loop Periodic;
The following example shows how to compensate for the inaccuracy of the delay
statement. This approach works well when the periodic requirement can be
satisfied with an average period. Periodic tasks based on an inaccurate delay
can drift from their phase. Prevention of this drift can be achieved by
calculating the next time-to-occur based on the actual time of the current
execution. The following example illustrates this tactic:
No_Drift:
declare
use Calendar;
-- Interval is a global constant of type Duration
Next_Time : Calendar.Time := Calendar.Clock + Interval;
begin -- No_Drift
Stable_Periodic:
loop
delay Next_Time - Clock;
...
Next_Time := Next_Time + Interval;
end loop Stable_Periodic;
end No_Drift;
rationale
The Ada language definition only guarantees that the delay time is a minimum.
The meaning of a delay statement is that the task is not scheduled for
execution before the interval has expired. In other words, a task becomes
eligible to resume execution as soon as the amount of time has passed.
However, there is no guarantee of when (or if) it is scheduled after that
time.
A busy wait can interfere with processing by other tasks. It can consume the
very processor resource necessary for completion of the activity for which it
is waiting. Even a loop with a delay can have the impact of busy waiting if
the planned wait is significantly longer then the delay interval. If a task
has nothing to do, it should be blocked at an accept or select statement.
Using knowledge of the execution pattern of tasks to achieve timing
requirements is nonportable. Ada does not specify the underlying scheduling
algorithm.
6.2 COMMUNICATION
The need for tasks to communicate gives rise to most of the problems that make
concurrent programming so difficult. Used properly, Ada's intertask
communication features can improve the reliability of concurrent programs;
used thoughtlessly, they can introduce subtle errors that can be difficult to
detect and correct.
6.2.1 Efficient Task Communications
guideline
o Minimize the work performed during a rendezvous.
o Minimize the work performed in the selective wait loop of a task.
example
In the following example, the statements in the accept body are performed as
part of the execution of both the caller task and the task Server which
contains Operation and Operation2. The statements after the accept body are
executed before Server can accept additional calls to Operation or Operation2.
...
loop
select
accept Operation do
-- These statements are executed during rendezvous.
-- Both caller and server are blocked during this time.
...
end Operation;
...
-- These statements are not executed during rendezvous.
-- Their execution delays getting back to the accept and
-- may be a candidate for another task.
or
accept Operation_2 do
-- These statements are executed during rendezvous.
-- Both caller and server are blocked during this time.
...
end Operation_2;
end select;
-- These statements are also not executed during rendezvous,
-- Their execution delays getting back to the accept and may
-- be a candidate for another task.
end loop;
rationale
Only work that needs to be performed during a rendezvous, such as saving or
generating parameters, should be allowed in the accept bodies to minimize the
time required to rendezvous.
When work is removed from the accept body and placed later in the selective
wait loop, the additional work may still suspend the caller task. If the
caller task calls entry Operation again before the server task completes its
additional work, the caller is delayed until the server completes the
additional work. If the potential delay is unacceptable and the additional
work does not need to be completed before the next service of the caller task,
the additional work may form the basis of a new task that will not block the
caller task.
note
In some cases, additional functions may be added to a task. For example, a
task controlling a communication device may be responsible for a periodic
function to ensure that the device is operating correctly. This type of
addition should be done with care realizing that the response time of the task
is impacted (see rationale).
exceptions
Task communication overhead must be balanced with the associated blocking.
Each time a new task is introduced, there is a timing impact caused by
scheduling and synchronization with the new task. Be careful when introducing
tasks to reduce blocking. The reduction in blocking time will cause increased
task scheduling and synchronization overhead and software architecture
complexity.
6.2.2 Defensive Task Communication
guideline
o Provide a handler for exception Program_Error whenever you cannot
avoid a selective wait statement whose alternates can all be closed (Honeywell
1986).
o Make systematic use of handlers for Tasking_Error.
o Be prepared to handle exceptions during a rendezvous.
example
This block allows recovery from exceptions raised while attempting to
communicate a command to another task.
Accelerate:
begin
Throttle.Increase(Step);
exception
when Tasking_Error => ...
when Constraint_Error |
Numeric_Error => ...
when Throttle_Too_Wide => ...
...
end Accelerate;
In this select statement, if all the guards happen to be closed, the program
can continue by executing the else part. There is no need for a handler for
Program_Error. Other exceptions can still be raised while evaluating the
guards or attempting to communicate.
...
Guarded:
begin
select
when Condition_1 =>
accept Entry_1;
or
when Condition_2 =>
accept Entry_2;
else -- all alternatives closed
...
end select;
exception
when Constraint_Error | Numeric_Error =>
...
end Guarded;
In this select statement, if all the guards happen to be closed, exception
Program_Error will be raised. Other exceptions can still be raised while
evaluating the guards or attempting to communicate.
Guarded:
begin
select
when Condition_1 =>
accept Entry_1;
or
when Condition_2 =>
delay Fraction_Of_A_Second;
end select;
exception
when Program_Error => ...
when Constraint_Error |
Numeric_Error => ...
end Guarded;;
...
rationale
The exception Program_Error is raised if a selective wait statement (select
statement containing accepts) is reached, all of whose alternatives are closed
(i.e., the guards evaluate to False and there are no alternatives without
guards), unless there is an else part. When all alternatives are closed, the
task can never again progress, so there is by definition an error in its
programming. You must be prepared to handle this error should it occur.
Since an else part cannot have a guard, it can never be closed off as an
alternative action, thus its presence prevents Program_Error. However, an else
part, a delay alternative, and a terminate alternative are all mutually
exclusive, so you will not always be able to provide an else part. In these
cases, you must be prepared to handle Program_Error.
The exception Tasking_Error can be raised in the calling task whenever it
attempts to communicate. There are many situations permitting this. Few of
them are preventable by the calling task.
If an exception is raised during a rendezvous and not handled in the accept
statement, it is propagated to both tasks and must be handled in two places.
See Guideline .
note
There are other ways to prevent Program_Error at a selective wait. These
involve leaving at least one alternative unguarded, or proving that at least
one guard will evaluate True under all circumstances. The point here is that
you, or your successors, will make mistakes in trying to do this, so you
should prepare to handle the inevitable exception.
6.2.3 Attributes 'Count, 'Callable and 'Terminated
guideline
o Do not depend on the values of the task attributes 'Callable or
'Terminated (Nissen and Wallis 1984).
o Do not depend on attributes to avoid Tasking_Error on an entry call.
o Do not depend on the value of the entry attribute 'Count.
example
In the following examples Intercept'Callable is a boolean indicating if a call
can be made to the task Intercept without raising the exception Tasking_Error.
Launch'Count indicates the number of callers currently waiting at entry
Launch. Intercept'Terminated is a boolean indicating if the task Intercept is
in terminated state.
This task is badly programmed because it relies upon the values of the 'Count
attributes not changing between evaluating and acting upon them.
---------------------------------------------------------------------
task body Intercept is
...
select
when Launch'Count > 0 and Recall'Count = 0 =>
accept Launch;
...
or
accept Recall;
...
end select;
...
end Intercept;
---------------------------------------------------------------------
If the following code is preempted between evaluating the condition and
initiating the call, the assumption that the task is still callable may no
longer be valid.
...
if Intercept'Callable then
Intercept.Recall;
end if;
...
rationale
Attributes 'Callable, 'Terminated, and 'Count are all subject to race
conditions. Between the time you reference an attribute and the time you take
action the value of the attribute may change. Attributes 'Callable and
'Terminated convey reliable information once they become False and True,
respectively. If 'Callable is False, you can expect the callable state to
remain constant. If 'Terminated is True, you can expect the task to remain
terminated. Otherwise, 'Terminated and 'Callable can change between the time
your code tests them and the time it responds to the result.
The Ada Language Reference Manual (Department of Defense 1983) itself warns
about the asynchronous increase and decrease of the value of 'Count. A task
can be removed from an entry queue due to execution of an abort statement as
well as expiration of a timed entry call. The use of this attribute in guards
of a selective wait statement may result in the opening of alternatives which
should not be opened under a changed value of 'Count.
6.2.4 Shared Variables
guideline
o Use the rendezvous mechanism, not shared variables, to pass data
between tasks.
o Do not use shared variables as a task synchronization device.
o Do not reference nonlocal variables in a guard.
example
This code will either print the same line more than once, fail to print some
lines, or print garbled lines (part of one line followed by part of another)
nondeterministically.
---------------------------------------------------------------------
task body Robot_Arm_Driver is
Current_Command : Robot_Command;
begin -- Robot_Arm_Driver
loop
Current_Command := Command;
-- send to device
end loop;
...
end Robot_Arm_Driver;
---------------------------------------------------------------------
task body Stream_Server is
begin
loop
Stream_Read(Stream_File, Command);
end loop;
...
end Stream_Server;
---------------------------------------------------------------------
This code ensures that a missile cannot be fired unless the doors are open and
that the missile cannot be armed unless the doors are shut. In this case the
requirement for arming may be derived from the duration that the door may be
open (i.e., arm first, open door, launch, close door).
Doors_Open : Boolean := False;
---------------------------------------------------------------------
task body Intercept is
begin
...
select
when Doors_Open = True =>
accept Launch;
...
or
when Doors_Open = False =>
accept Arm;
...
end select;
...
end Intercept;
---------------------------------------------------------------------
---------------------------------------------------------------------
task body Intercept is
Local_Doors_Open : Boolean := False;
begin -- Intercept
...
select
when Local_Doors_Open = True =>
accept Launch;
...
or
when Local_Doors_Open = False =>
accept Arm;
...
or
accept Door_Status
(Doors_Open : in Boolean) do
Local_Doors_Open := Doors_Open;
end Door_Status;
end select;
...
end Intercept;
---------------------------------------------------------------------
rationale
There are many techniques for protecting and synchronizing data access. You
must program most of them yourself to use them. It is difficult to write a
program that shares data correctly. If it is not done correctly, the
reliability of the program suffers. Ada provides the rendezvous to support
synchronization and communication of information between tasks. Data that you
might be tempted to share can be put into a task body with read and write
entries to access it.
The first example above has a race condition requiring perfect interleaving of
execution. This code can be made more reliable by introducing a flag that is
set by Spool_Server and reset by Line_Printer_Driver. An if (condition flag)
then delay ... else would be added to each task loop in order to ensure that
the interleaving is satisfied. However, notice that this approach requires a
delay and the associated rescheduling. Presumably this rescheduling overhead
is what is being avoided by not using the rendezvous.
A guard is a conditional select alternative starting with a when (see 9.7.1 in
Department of Defense 1983). The second example above also has a race
condition requiring two different things. First, the task that opens the doors
must open the doors and update Doors_Open before allowing the intercept task
to continue execution. Second, the run time system evaluation of the guard in
the select statement cannot occur until the Doors_Open matches the next
anticipated entry call. If the next call will be to ARM, then you must make
sure that Doors_Open changes to False before the Intercept task reevaluates
the select statement. If the select statement is evaluated while Doors_Open is
True and Doors_Open is subsequently set to False, the select will continue to
wait on the Launch until a Launch is received. An alternate approach is to use
Local_Doors_Open in the example. This guarantees that the guards will be
reevaluated upon a change in the value of Doors_Open.
exceptions
For some required synchronizations the rendezvous may not meet time
constraints. Each case should be analyzed in detail to justify the use of
pragma Shared, which presumably has less overhead than the rendezvous. Be
careful to correctly implement a data access synchronization technique.
Without great effort you might get it wrong. Pragma Shared can serve as an
expedient against poor run time support systems. Do not always use this as an
excuse to avoid the rendezvous because implementations are allowed to ignore
pragma Shared (Nissen and Wallis 1984). When pragma Shared is implemented by
compilers, the implementation is not always uniform and can still lead to
nonportable code. Pragma Shared affects only those objects whose storage and
retrieval are implemented as indivisible operations. Also, pragma Shared can
only be used for variables of scalar or access type.
note
As pointed out above, a guarantee of noninterference may be difficult with
implementations that ignore pragma Shared. If you must share data, share the
absolute minimum amount necessary, and be especially careful. As always,
encapsulate the synchronization portions of code.
The problem is with variables. Constants, such as tables fixed at compile
time, may be safely shared between tasks.
6.2.5 Tentative Rendezvous Constructs
guideline
o Use caution with conditional entry calls.
o Use caution with selective waits with else parts.
o Do not depend upon a particular delay in timed entry calls.
o Do not depend upon a particular delay in selective waits with delay
alternatives.
example
The conditional entry call in the following code results in a race condition
that may degenerate into a busy waiting loop. The task Current_Position
containing entry Request_New_Coordinates may never execute if this task has a
higher priority than Current_Position, because this task doesn't release the
processing resource.
...
loop
select
Current_Position.Request_New_Coordinates(X, Y);
-- calculate target location based on new coordinates
...
else
-- calculate target location based on last locations
...
end select;
end loop;
...
The addition of a delay as shown may allow Current_Position to execute until
it reaches an accept for Request_New_Coordinates.
...
loop
select
Current_Position.Request_New_Coordinates(X, Y);
-- calculate target location based on new coordinates
...
else
-- calculate target location based on last locations
...
delay Next_Execute - Clock;
Next_Execute := Next_Execute + Period;
end select;
end loop;
...
The following selective wait with else again does not degenerate into a busy
wait loop only because of the addition of a delay statement.
loop
delay Next_Execute - Clock;
select
accept Get_New_Message (Message : in String) do
-- copy message to parameters
...
end Get_New_Message;
else -- Don't wait for rendezvous
-- perform built in test Functions
...
end select;
Next_Execute := Next_Execute + Task_Period;
end loop;
The following timed entry call may be considered an unacceptable
implementation if lost communications with the reactor for over 25
milliseconds results in a critical situation.
...
loop
select
Reactor.Status(OK);
or
delay 0.025;
-- lost communication for more that 25 milliseconds
Emergency_Shutdown;
end select;
-- process reactor status
...
end loop;
...
In the following "selective wait with delay" example, the accuracy of the
coordinate calculation function is bounded by time. For example, the required
accuracy cannot be obtained unless Period is within + or - 0.005 seconds. This
period cannot be guaranteed because of the inaccuracy of the delay statement.
...
loop
select
accept Request_New_Coordinates (X : out Integer;
Y : out Integer) do
-- copy coordinates to parameters
...
end Request_New_Coordinates;
or
delay Next_Execute - Calendar.Clock;
end select;
Next_Execute := Next_Execute + Period;
-- Read Sensors
-- execute coordinate transformations
end loop;
...
rationale
Use of these constructs always poses a risk of race conditions. Using them in
loops, particularly with poorly chosen task priorities, can have the effect of
busy waiting.
These constructs are very much implementation dependent. For conditional entry
calls and selective waits with else parts, the Ada Language Reference Manual
(Department of Defense 1983) does not define "immediately." For timed entry
calls and selective waits with delay alternatives, implementors may have ideas
of time that differ from each other and from your own. Like the delay
statement, the delay alternative on the select construct might wait longer
than the time required (see Guideline ).
6.2.6 Communication Complexity
guideline
o Minimize the number of accept and select statements per task.
o Minimize the number of accept statements per entry.
example
Use
accept A;
if Mode_1 then
-- do one thing
else -- Mode_2
-- do something different
end if;
rather than
if Mode_1 then
accept A;
-- do one thing
else -- Mode_2
accept A;
-- do something different
end if;
rationale
This guideline reduces conceptual complexity. Only entries necessary to
understand externally observable task behavior should be introduced. If there
are several different accept and select statements that do not modify task
behavior in a way important to the user of the task, there is unnecessary
complexity introduced by the proliferation of select/accept statements.
Externally observable behavior important to the task user includes task timing
behavior, task rendezvous initiated by the entry calls, prioritization of
entries, or data updates (where data is shared between tasks).
6.3 TERMINATION
The ability of tasks to interact with each other using Ada's intertask
communication features makes it especially important to manage planned or
unplanned (e.g., in response to a catastrophic exception condition)
termination in a disciplined way. To do otherwise can lead to a proliferation
of undesired and unpredictable side effects as a result of the termination of
a single task.
6.3.1 Avoiding Termination
guideline
o Place an exception handler for a rendezvous within the main tasking
loop.
example
In the following example an exception raised using the primary sensor is used
to change Mode to Degraded still allowing execution of the system.
...
loop
Recognize_Degraded_Mode:
begin
if Mode = Primary then
select
Current_Position_Primary.Request_New_Coordinates
(X, Y);
or
delay 0.25;
-- Decide whether to switch modes;
end select;
else -- Mode = Degraded
Current_Position_Backup.Request_New_Coordinates
(X, Y);
end if;
...
exception
when Tasking_Error | Program_Error =>
Mode := Degraded;
end Recognize_Degraded_Mode;
end loop;
...
rationale
Allowing a task to terminate may not support the requirements of the system.
Without an exception handler for the rendezvous within the main task loop, the
functions of the task may not be performed.
note
The use of an exception handler is the only way to guarantee recovery from an
entry call to an abnormal task. Use of the 'Terminated attribute to test a
task's availability before making the entry call can introduce a race
condition where the tested task fails after the test but before the entry call
(see Guideline ).
6.3.2 Normal Termination
guideline
o Do not create non-terminating tasks unintentionally.
o Explicitly shut down tasks dependent on library packages.
o Use a select statement rather than an accept statement alone.
o Provide a terminate alternative for every selective wait that does not
require an else part or a delay.
example
This task will never terminate:
---------------------------------------------------------------------
task body Message_Buffer is
...
begin -- Message_Buffer
loop
select
when Head /= Tail => -- Circular buffer not empty
accept Retrieve (Value : out Element) do
...
end Retrieve;
or
when not ((Head = Lower_Bound and then
Tail = Upper_Bound) or else
(Head /= Lower_Bound and then
Tail = Index'Pred(Head)) )
=> -- Circular buffer not full
accept Store (Value : in Element);
end select;
end loop;
...
end Message_Buffer;
---------------------------------------------------------------------
rationale
A nonterminating task is a task whose body consists of a nonterminating loop
with no selective wait with terminate, or a task that is dependent on a
library package. Execution of a subprogram or block containing a task cannot
complete until the task terminates. Any task that calls a subprogram
containing a nonterminating task will be delayed indefinitely.
The effect of unterminated tasks at the end of program execution is undefined.
A task dependent on a library package cannot be forced to terminate using a
selective wait construct with terminate alternative and should be terminated
explicitly during program shutdown. One way to terminate tasks dependent on
library packages is to provide them with exit entries. Have the main
subprogram call the exit entry just before it terminates.
Execution of an accept statement or of a selective wait statement without an
else part, a delay, or a terminate alternative cannot proceed if no task ever
calls the entry(s) associated with that statement. This could result in
deadlock. Following this guideline entails programming multiple termination
points in the task body. A reader can easily "know where to look" for the
normal termination points in a task body. The termination points are the end
of the body's sequence of statements, and alternatives of select statements.
exceptions
If you are simulating a cyclic executive, you may need a scheduling task that
does not terminate. It has been said that no real-time system should be
programmed to terminate. This is extreme. Systematic shutdown of many real-time
systems is a desirable safety feature.
If you are considering programming a task not to terminate, be certain that it
is not a dependent of a block or subprogram from which the task's caller(s)
will ever expect to return. Since entire programs can be candidates for reuse
(see Chapter ), note that the task (and whatever it depends upon) will not
terminate. Also be certain that for any other task that you do wish to
terminate, its termination does not await this task's termination. Reread and
fully understand paragraph 9.4 of Department of Defense (1983) on "Task
Dependence - Termination of Tasks."
6.3.3 The Abort Statement
guideline
o Avoid using the abort statement.
example
If required in the application, provide a task entry for orderly shutdown.
rationale
When an abort statement is executed, there is no way to know what the targeted
task was doing beforehand. Data for which the target task is responsible may
be left in an inconsistent state. The overall effect on the system of aborting
a task in such an uncontrolled way requires careful analysis. The system
design must ensure that all tasks depending on the aborted task can detect the
termination and respond appropriately.
Tasks are not aborted until they reach a synchronization point such as
beginning or end of elaboration, a delay statement, an accept statement, an
entry call, a select statement, task allocation, or the execution of an
exception handler. Consequently, the abort statement may not release processor
resources as soon as you may expect. It also may not stop a runaway task
because the task may be executing an infinite loop containing no
synchronization points.
6.3.4 Abnormal Termination
guideline
o Place an exception handler for others at the end of a task body.
o Have each exception handler at the end of a task body report the
task's demise.
example
This is one of many tasks updating the positions of blips on a radar screen.
When started, it is given part of the name by which its parent knows it.
Should it terminate due to an exception, it signals the fact in one of its
parent's data structures.
---------------------------------------------------------------------
task body Track is
My_Index : Track_Index;
Neutral : Boolean := True;
begin -- Track
select
accept Start (Who_Am_I : in Track_Index) do
My_Index := Who_Am_I;
end Start;
Neutral := False;
...
or
terminate;
end select;
...
exception
when others =>
if not Neutral then
Station(My_Index).Status := Dead;
end if;
end Track;
---------------------------------------------------------------------
rationale
A task will terminate if an exception is raised within it for which it has no
handler. In such a case, the exception is not propagated outside of the task
(unless it occurs during a rendezvous). The task simply dies with no
notification to other tasks in the program. Therefore, providing exception
handlers within the task, and especially a handler for others, ensures that a
task can regain control after an exception occurs. If the task cannot proceed
normally after handling an exception, this affords it the opportunity to shut
itself down cleanly and to notify tasks responsible for error recovery
necessitated by the abnormal termination of the task.
note
Do not use the task status to determine if a rendezvous can be made with the
task. If task A is dependent on task B and task A checks the status flag
before it rendezvous with task B, there is a potential that task B fails
between the status test and the rendezvous. In this case, task A must provide
an exception handler to handle the Tasking_Error exception raised by the call
to an entry of an abnormal task (see Guideline ).
6.4 SUMMARY
tasking
o Use tasks to model asynchronous entities within the problem domain.
o Use tasks to control or synchronize access to tasks or devices.
o Use tasks to define concurrent algorithms.
o Use anonymous task types for single instances.
o Justify the use of dynamically allocated task objects.
o Avoid disassociating a dynamic task from all names.
o Do not rely on pragma Priority to prioritize the service of entries.
o Arrange task bodies in order of their priorities (if possible).
o Do not depend on a particular delay being achievable.
o Do not use a busy waiting loop instead of a delay.
o Design to limit polling to those cases where absolutely necessary.
o Do not use knowledge of the execution pattern of tasks to achieve
timing requirements.
communication
o Minimize the work performed during a rendezvous.
o Minimize the work performed in the selective wait loop of a task.
o Provide a handler for exception Program_Error whenever you cannot
avoid a selective wait statement whose alternates can all be closed.
o Make systematic use of handlers for Tasking_Error.
o Be prepared to handle exceptions during a rendezvous.
o Do not depend on the values of the task attributes 'Callable or
'Terminated.
o Do not depend on attributes to avoid Tasking_Error on an entry call.
o Do not depend on the value of the entry attribute 'Count.
o Use the rendezvous mechanism, not shared variables, to pass data
between tasks.
o Do not use shared variables as a task synchronization device.
o Do not reference nonlocal variables in a guard.
o Use caution with conditional entry calls.
o Use caution with selective waits with else parts.
o Do not depend upon a particular delay in timed entry calls.
o Do not depend upon a particular delay in selective waits with delay
alternatives.
o Minimize the number of accept and select statements per task.
o Minimize the number of accept statements per entry.
termination
o Place an exception handler for a rendezvous within the main tasking
loop.
o Do not create non-terminating tasks unintentionally.
o Explicitly shut down tasks dependent on library packages.
o Use a select statement rather than an accept statement alone.
o Provide a terminate alternative for every selective wait that does not
require an else part or a delay.
o Avoid using the abort statement.
o Place an exception handler for others at the end of a task body.
o Have each exception handler at the end of a task body report the
task's demise.
CHAPTER 7
Portability
Discussions concerning portability usually concentrate on the differences in
computer systems. But the development and runtime environment may also change:
portability (software). The ease with which software can be transferred from
one computer system or environment to another (IEEE Dictionary 1984).
And most portability problems are not pure language issues. Portability
involves hardware (byte order, device IO); software (utility libraries,
operating systems, runtime libraries). This section will not address these
challenging design issues.
This section does identify the more common portability problems that are
specific to Ada when moving from one platform or compiler to another. It also
suggests ways that non-portable code can be isolated. By using the
implementation hiding features of Ada, the cost of porting can be
significantly reduced.
In fact, many language portability issues are solved by the strict definition
of the Ada language itself. In most programming languages, different dialects
are prevalent as vendors extend or dilute a language for various reasons:
conformance to a programming environment; or features for a particular
application domain. The Ada Compiler Validation Capability (ACVC) was
developed by the U.S. Department of Defense at the Ada Validation Facility,
ASD/SIDL, Wright-Patterson Air Force Base to ensure that implementors strictly
adhered to the Ada standard.
As part of the strict definition of Ada, certain constructs are defined to be
erroneous and the effect of executing an erroneous construct is unpredictable.
Therefore erroneous constructs are obviously not portable. Erroneous
constructs are discussed in Guideline 5.9, and are not repeated in this
chapter.
Most programmers new to the language expect Ada to eliminate all portability
problems; it definitely does not. Certain areas of Ada are not yet covered by
validation. The definition of Ada leave certain details to the implementor.
The compiler implementor's choices with respect to these details affect
portability.
There are some general principles to enhancing portability exemplified by many
of the guidelines in this chapter. They are:
o Recognize those Ada constructs that may adversely affect portability.
o Avoid the use of these constructs where possible.
o Localize and encapsulate nonportable features of a program if their
use is essential.
o Highlight the use of constructs that may cause portability problems.
These guidelines cannot be applied thoughtlessly. Many of them involve a
detailed understanding of the Ada model and its implementation. In many cases,
you will have to make carefully considered tradeoffs between efficiency and
portability. Reading this chapter should improve your insight into the
tradeoffs involved.
The material in this chapter was largely acquired from three sources: the Ada
Run Time Environment Working Group (ARTEWG) Catalogue of Ada Run Time
Implementation Dependencies (ARTEWG 1986); the Nissen and Wallis book on
Portability and Style in Ada (Nissen and Wallis 1984); and a paper written for
the U.S. Air Force by SofTech on Ada Portability Guidelines (Pappas 1985). The
last of these sources (Pappas 1985) encompasses the other two and provides an
in depth explanation of the issues, numerous examples, and techniques for
minimizing portability problems. Conti (1987) is a valuable reference for
understanding the latitude allowed for implementors of Ada and the criteria
often used to make decisions.
This chapter's purpose is to provide a summary of portability issues in the
guideline format of this book. The chapter does not include all issues
identified in the references, but only the most significant. For an in-depth
presentation, see Pappas (1985). A few additional guidelines are presented
here and others are elaborated upon where applicable. For further reading on
Ada I/O portability issues, see Matthews (1987), Griest (1989), and CECOM
(1989).
Some of the guidelines in this chapter cross reference and place stricter
constraints on other guidelines in this book. These constraints apply when
portability is being emphasized.
7.1 FUNDAMENTALS
This section introduces some generally applicable principles of writing
portable Ada programs. It includes guidelines about the assumptions you should
make with respect to a number of Ada features and their implementations and
guidelines about the use of other Ada features to ensure maximum portability.
7.1.1 Global Assumptions
guideline
o Make considered assumptions about the support provided for the
following on potential target platforms:
- Number of bits available for type Integer (range constraints).
- Number of decimal digits of precision available for floating point
types.
- Number of bits available for fixed-point types (delta and range
constraints).
- Number of characters per line of source text.
- Number of bits for Universal_Integer expressions.
- Number of seconds for the range of Duration.
- Number of milliseconds for Duration'Small.
instantiation
These are minimum values (or minimum precision in the case of Duration'Small)
that a project or application might assume that an implementation provides.
There is no guarantee that a given implementation provides more than the
minimum, so these would be treated by the project or application as maximum
values also.
- 16 bits available for type Integer (-2**15 .. 2**15 - 1).
- 6 decimal digits of precision available for floating point types.
- 32 bits available for fixed-point types.
- 72 characters per line of source text.
- 16 bits for Universal_Integer expressions.
- -86_400 .. 86_400 seconds (1 day) for the range of Duration (as
specified in 9.6 (4) of Department of Defense 1983))
- 20 milliseconds for Duration'Small (as specified in 9.6 (4) of
Department of Defense 1983)).
rationale
Some assumptions must be made with respect to certain implementation dependent
values. The exact values assumed should cover the majority of the target
equipment of interest. Choosing the lowest common denominator for values
improves portability.
note
Of the microcomputers currently available for incorporation within embedded
systems, 16-bit and 32-bit processors are prevalent. Although 4-bit and 8-bit
machines are still available, their limited memory addressing capabilities
make them unsuited to support Ada programs of any size. Using current
representation schemes, 6 decimal digits of floating point accuracy implies a
representation mantissa at least 21 bits wide, leaving 11 bits for exponent
and sign within a 32-bit representation. This correlates with the data widths
of floating point hardware currently available for the embedded systems
market. A 32-bit minimum on fixed-point numbers correlates with the accuracy
and storage requirements of floating point numbers.
The 72-column limit on source lines in the example is an unfortunate hold-over
from the days of Hollerith punch cards with sequence numbers. There may still
be machinery and software used in manipulating source code that are bound to
assumptions about this maximum line length. The 16-bit example for
Universal_Integer expressions matches that for Integer storage.
The values for the range and accuracy of values of the predefined type
Duration are the limits expressed in the Ada Language Reference Manual
(Department of Defense 1983, w 9.6). You should not expect an implementation
to provide a wider range or a finer granularity.
7.1.2 Actual Limits
guideline
o Don't implicitly use an implementation limit.
rationale
The Ada model may not match exactly with the underlying hardware, so some
compromises may have been made in the implementation. Check to see if they
could affect your program. Particular implementations may do "better" than the
Ada model requires while some others may be just minimally acceptable.
Arithmetic is generally implemented with a precision higher than the storage
capacity (this is implied by the Ada type model for floating point). Different
implementations may behave differently on the same underlying hardware.
7.1.3 Comments
guideline
o Use highlighting comments for each package, subprogram and task where
any nonportable features are present.
o For each nonportable feature employed, describe the expectations for
that feature.
example
------------------------------------------------------------------------
package Memory_Mapped_IO is
-- WARNING - This package is implementation specific.
-- It uses absolute memory addresses to interface with the I/O
-- system. It assumes a particular printer's line length.
-- Change memory mapping and printer details when porting.
Printer_Line_Length : constant := 132;
type Data is array (1 .. Printer_Line_Length) of Character;
procedure Write_Line (Line : in Data);
end Memory_Mapped_IO;
------------------------------------------------------------------------
with System;
package body Memory_Mapped_IO is
---------------------------------------------------------------------
procedure Write_Line (Line : in Data) is
Buffer : Data;
for Buffer use at System.Physical_Address(16#200#);
begin -- Write_Line
-- perform output operation through specific memory locations.
...
end Write_Line;
---------------------------------------------------------------------
end Memory_Mapped_IO;
------------------------------------------------------------------------
rationale
Explicitly commenting each breach of portability will raise its visibility and
aid in the porting process. A description of the non-portable feature's
expectations covers the common case where vendor documentation of the original
implementation is not available to the person performing the porting process.
7.1.4 Main Subprogram
guideline
o Use only a parameterless procedure as the main program.
rationale
The Ada Language Reference Manual (Department of Defense 1983) places very few
requirements on the main subprogram. Assume the simplest case will increase
portability. That is, assume you may only use a parameterless procedure as a
main program.
Some operating systems are capable of acquiring and interpreting returned
integer values near zero from a function, but many others cannot. Further,
many real-time, embedded systems will not be designed to terminate, so a
function or a procedure having parameters with modes out or in out will be
inappropriate to such applications.
This leaves procedures with in parameters. Although some operating systems can
pass parameters in to a program as it starts, others cannot. Also, an
implementation may not be able to perform type checking on such parameters
even if the surrounding environment is capable of providing them.
note
Real-time, embedded applications may not have an "operator" initiating the
program to supply the parameters, in which case it would be more appropriate
for the program to have been compiled with a package containing the
appropriate constant values or for the program to read the necessary values
from switch settings or a downloaded auxiliary file. In any case, the
variation in surrounding initiating environments is far too great to depend
upon the kind of last-minute (program) parameterization implied by
(subprogram) parameters to the main subprogram.
7.1.5 Encapsulating Implementation Dependencies
guideline
o Create packages specifically designed to isolate hardware and
implementation dependencies and designed so that their specification will not
change when porting.
o Clearly indicate the objectives if machine or solution efficiency is
the reason for hardware or implementation dependent code.
o For the packages that hide implementation dependencies, maintain
different package bodies for different target environments.
o Isolate interrupt receiving tasks into implementation dependent
packages.
example
See Guideline .
rationale
Encapsulating hardware and implementation dependencies in a package allows the
remainder of the code to ignore them and thus to be fully portable. It also
localizes the dependencies, making it clear exactly which parts of the code
may need to change when porting the program.
Some implementation-dependent features may be used to achieve particular
performance or efficiency objectives. Commenting these objectives ensures that
the programmer can find an appropriate way to achieve them when porting to a
different implementation, or explicitly recognize that they cannot be achieved.
Interrupt entries are implementation-dependent features that may not be
supported (e.g., VAX Ada uses pragmas to assign system traps to "normal"
rendezvous). However, interrupt entries cannot be avoided in most embedded
real-time systems and it is reasonable to assume that they are supported by an
Ada implementation. The value for an interrupt is implementation-defined.
Isolate it.
note
Ada can be used to write machine-dependent programs that take advantage of an
implementation in a manner consistent with the Ada model, but which make
particular choices where Ada allows implementation freedom. These machine
dependencies should be treated in the same way as any other implementation
dependent features of the code.
7.1.6 Implementation-Added Features
guideline
o Avoid the use of vendor supplied packages.
o Avoid the use of features added to the predefined packages that are
not specified in the language definition.
rationale
Vendor-added features are not likely to be provided by other implementations.
Even if a majority of vendors eventually provide similar additional features,
they are unlikely to have identical formulations. Indeed, different vendors
may use the same formulation for (semantically) entirely different features.
exceptions
There are many types of applications that require the use of these features.
Examples include: multilingual systems that standardize on a vendor's file
system, applications that are closely integrated with vendor products (i.e.,
user interfaces), and embedded systems for performance reasons. Isolate the
use of these features into packages.
7.2 NUMERIC TYPES AND EXPRESSIONS
A great deal of care was taken with the design of the Ada features related to
numeric computations to ensure that the language could be used in embedded
systems and mathematical applications where precision was important. As far as
possible, these features were made portable. However, there is an inevitable
tradeoff between maximally exploiting the available precision of numeric
computation on a particular machine and maximizing the portability of Ada
numeric constructs. This means that these Ada features, particularly numeric
types and expressions, must be used with great care if full portability of the
resulting program is to be guaranteed.
7.2.1 Predefined Numeric Types
guideline
o Do not use the predefined numeric types in package Standard. Use range
and digits declarations and let the implementation do the derivation
implicitly from the predefined types.
o For programs that require greater accuracy than that provided by the
global assumptions, define a package that declares a private type and
operations as needed; see Pappas (1985) for a full explanation and examples.
example
The second and third examples below are not representable as subranges of
Integer on a machine with a 16-bit word. The first example below allows a
compiler to choose a multiword representation if necessary.
use
type Second_Of_Day is range 0 .. 86_400;
rather than
type Second_Of_Day is new Integer range 1 .. 86_400;
or
subtype Second_Of_Day is Integer range 1 .. 86_400;
rationale
An implementor is free to define the range of the predefined numeric types.
Porting code from an implementation with greater accuracy to one of lesser is
a time consuming and error-prone process. Many of the errors are not reported
until run-time.
This applies to more than just numerical computation. An easy-to-overlook
instance of this problem occurs if you neglect to use explicitly declared
types for integer discrete ranges (array sizes, loop ranges, etc.) (see
Guidelines and ). If you do not provide an explicit type when specifying index
constraints and other discrete ranges, a predefined integer type is assumed.
exceptions
Any indexing into the predefined String type requires that the index at least
be a subtype of the predefined Integer type. The predefined packages also use
the various predefined types.
note
There is an alternative which this guideline permits. As Guideline suggests,
implementation dependencies can be encapsulated in packages intended for that
purpose. This could include the definition of a 32-bit integer type. It
would then be possible to derive additional types from that 32-bit type.
7.2.2 Ada Model
guideline
o Know the Ada model for floating point types and arithmetic.
rationale
Declarations of Ada floating point types give users control over both the
representation and arithmetic used in floating point operations. Portable
properties of Ada programs are derived from the models for floating point
numbers of the subtype and the corresponding safe numbers. The relative
spacing and range of values in a type are determined by the declaration.
Attributes can be used to specify the transportable properties of an Ada
floating point type.
7.2.3 Analysis
guideline
o Carefully analyze what accuracy and precision you really need.
rationale
Floating point calculations are done with the equivalent of the
implementation's predefined floating point types. The effect of extra "guard"
digits in internal computations can sometimes lower the number of digits that
must be specified in an Ada declaration. This may not be consistent over
implementations where the program is intended to be run. It may also lead to
the false conclusion that the declared types are sufficient for the accuracy
required.
The numeric type declarations should be chosen to satisfy the lowest precision
(smallest number of digits) that will provide the required accuracy. Careful
analysis will be necessary to show that the declarations are adequate.
7.2.4 Accuracy Constraints
guideline
o Do not press the accuracy limits of the machine(s).
rationale
The Ada floating point model is intended to facilitate program portability,
which is often at the expense of efficiency in using the underlying machine
arithmetic. Just because two different machines use the same number of digits
in the mantissa of a floating point number does not imply they will have the
same arithmetic properties. Some Ada implementations may give slightly better
accuracy than required by Ada because they make efficient use of the machine.
Do not write programs that depend on this.
7.2.5 Comments
guideline
o Comment the analysis and derivation of the numerical aspects of a
program.
rationale
Decisions and background about why certain precisions are required in a
program are important to program revision or porting. The underlying numerical
analysis leading to the program should be commented.
7.2.6 Precision of Constants
guideline
o Use named numbers or universal real expressions rather than constants
of any particular type.
rationale
For a given radix (number base), there is a loss of accuracy for some rational
and all irrational numbers when represented by a finite sequence of digits.
Ada has named numbers and expressions of type universal_real that provide
maximal accuracy of representation in the source program. These numbers and
expressions are converted to finite representations at compile time. By using
universal real expressions and numbers, the programmer can automatically delay
the conversion to machine types until the point where it can be done with the
minimum loss of accuracy.
note
See also Guideline .
7.2.7 Subexpression Evaluation
guideline
o Anticipate values of subexpressions to avoid exceeding the range of
their type. Use derived types, subtypes, factoring, and range constraints on
numeric types as described in Guidelines , , and .
rationale
The Ada language does not require that an implementation perform range checks
on subexpressions within an expression. Even if the implementation on your
program's current target does not perform these checks, your program may be
ported to an implementation that does.
7.2.8 Relational Tests
guideline
o Use <= and >= to do relational tests on real valued arguments,
avoiding the <, >, =, and /= operations.
o Use values of type attributes in comparisons and checking for small
values.
example
The following examples test for (1) absolute "equality" in storage, (2)
absolute "equality" in computation, (3) relative "equality" in storage, and (4)
relative "equality" in computation.
abs (X - Y) <= Float_Type'Small -- (1)
abs (X - Y) <= Float_Type'Base'Small -- (2)
abs (X - Y) <= abs X * Float_Type'Epsilon -- (3)
abs (X - Y) <= abs X * Float_Type'Base'Epsilon -- (4)
And specifically for "equality" to zero:
abs X <= Float_Type'Small -- (1)
abs X <= Float_Type'Base'Small -- (2)
abs X <= abs X * Float_Type'Epsilon -- (3)
abs X <= abs X * Float_Type'Base'Epsilon -- (4)
rationale
Strict relational comparisons ( <, >, =, /= ) are a general problem in real
valued computations. Because of the way Ada comparisons are defined in terms
of model intervals, it is possible for the values of the Ada comparisons A < B
and A = B to depend on the implementation, while A <= B evaluates uniformly
across implementations. Note that for real values in Ada, "A <= B" is not the
same as "not (A > B)". Further explanation can be found in Cohen (1986)
pp.227-233.
Type attributes are the primary means of symbolically accessing the
implementation of the Ada numeric model. When the characteristics of the model
numbers are accessed symbolically, the source code is portable. The
appropriate model numbers of any implementation will then be used by the
generated code.
Although zero is technically not a special case, it is often overlooked
because it looks like the simplest and, therefore, safest case. But in
reality, each time comparisons involve small values, evaluate the situation to
determine which technique is appropriate.
note
Regardless of language, real valued computations have inaccuracy. That the
corresponding mathematical operations have algebraic properties usually
introduces some confusion. This guideline explains how Ada deals with the
problem that most languages face.
7.3 STORAGE CONTROL
The management of dynamic storage can vary between Ada environments. In fact,
some environments do not provide any deallocation. Ada's storage control
mechanisms are too implementation dependent to be considered portable.
7.3.1 Representation Clause
guideline
o Do not use a representation clause to specify number of storage units.
rationale
The meaning of the 'Storage_Size attribute is ambiguous; so, specifying a
particular value will not improve portability. It may or may not include
space allocated for parameters, data, etc. Save the use of this feature for
designs that must depend on a particular vendor's implementation.
note
During a porting activity, it can be assumed that any occurrence of storage
specification indicates an implementation dependency that must be redesigned.
7.4 TASKING
The definition of tasking in the Ada language leaves many characteristics of
the tasking model up to the implementor. This allows a vendor to make
appropriate tradeoffs for the intended application domain, but it also
diminishes the portability of designs and code employing the tasking features.
In some respects this diminished portability is an inherent characteristic of
concurrency approaches (see Nissen and Wallis 1984, 37).
A discussion of Ada tasking dependencies when employed in a distributed target
environment is beyond the scope of this book. For example, multi-processor task
scheduling, interprocessor rendezvous, and the distributed sense of time
through package Calendar are all subject to differences between
implementations. For more information, Nissen and Wallis (1984) and ARTEWG
(1986) touch on these issues and (Volz et al. 1985) is one of many research
articles available.
7.4.1 Task Activation Order
guideline
o Do not depend on the order in which task objects are activated when
declared in the same declarative list.
rationale
The order is left undefined in the Ada Language Reference Manual (Department
of Defense 1983).
7.4.2 Delay Statements
guideline
o Do not depend on a particular delay being achievable (Nissen and
Wallis 1984).
o Never use a busy waiting loop instead of a delay.
o Design to limit polling to those cases where it is absolutely
necessary.
o Never use knowledge of the execution pattern of tasks to achieve
timing requirements.
rationale
The rationale for this appears in Guideline . In addition, the treatment of
delay statements varies from implementation to implementation thereby
hindering portability.
7.4.3 Package Calendar, Type Duration, and System.Tick
guideline
o Do not assume a correlation between System.Tick and package Calendar
or type Duration (see Guideline ).
rationale
Such a correlation is not required, although it may exist in some
implementations.
7.4.4 Select Statement Evaluation Order
guideline
o Do not depend on the order in which guard conditions are evaluated or
on the algorithm for choosing among several open select alternatives.
rationale
The language does not define the order of these conditions, so assume that
they are arbitrary.
7.4.5 Task Scheduling Algorithm
guideline
o Do not assume that tasks execute uninterrupted until they reach a
synchronization point.
o Use pragma Priority to distinguish general levels of importance only
(see Guideline ).
rationale
The Ada tasking model requires that tasks be synchronized only through the
explicit means provided in the language (i.e., rendezvous, task dependence,
pragma Shared). The scheduling algorithm is not defined by the language and
may vary from time sliced to preemptive priority. Some implementations (e.g.,
VAX Ada) provide several choices that a user may select for the application.
note
The number of priorities may vary between implementations. In addition, the
manner in which tasks of the same priority are handled may vary between
implementations even if the implementations use the same general scheduling
algorithm.
exceptions
In real-time systems it is often necessary to tightly control the tasking
algorithm to obtain the required performance. For example, avionics systems
are frequently driven by cyclic events with limited asynchronous
interruptions. A nonpreemptive tasking model is traditionally used to obtain
the greatest performance in these applications. Cyclic executives can be
programmed in Ada, as can a progression of scheduling schemes from cyclic
through multiple-frame-rate to full asynchrony (MacLaren 1980) although an
external clock is usually required.
7.4.6 Abort
guideline
o Avoid using the abort statement.
rationale
The rationale for this appears in Guideline . In addition, treatment of the
abort statement varies from implementation to implementation thereby hindering
portability.
7.4.7 Shared Variables and Pragma Shared
guideline
o Do not share variables.
o Have tasks communicate through the rendezvous mechanism.
o Do not use shared variables as a task synchronization device.
o Use pragma Shared only when you are forced to by run time system
deficiencies.
rationale
The rationale for this appears in Guideline . In addition, the treatment of
shared variables varies from implementation to implementation thereby
hindering portability.
7.5 EXCEPTIONS
Care must be exercised using predefined exceptions since aspects of their
treatment may vary between implementations. Implementation-defined exceptions
must, of course, be avoided. Other guidelines concerning exceptions can be
found in Guidelines and .
7.5.1 Predefined Exceptions
guideline
o Do not depend on the exact locations at which predefined exceptions
are raised.
rationale
The Ada Language Reference Manual (Department of Defense 1983) states that
among implementations, a predefined exception for the same cause may be raised
from different locations. You will not be able to discriminate between the
exceptions. Further, each of the predefined exceptions is associated with a
variety of conditions. Any exception handler written for a predefined
exception must be prepared to deal with any of these conditions.
7.5.2 Constraint_Error and Numeric_Error
guideline
o Catch Numeric_Error exceptions with a Numeric_Error | Constraint_Error
exception handler.
o Do not use a separate exception handler for Numeric_Error and
Constraint_Error.
rationale
In cases where Numeric_Error may be raised, an implementation is allowed to
raise Constraint_Error instead. In fact, there is no requirement that an
implementation raise the same exception under the same circumstances. It is
not enough to replace the Numeric_Error exception with a Constraint_Error.
Either one may be raised; and if Numeric_Error is raised, it will not be
caught with a simple Constraint_Error exception handler.
7.5.3 Implementation-Defined Exceptions
guideline
o Do not raise implementation-defined exceptions.
o Convert implementation defined exceptions within interface packages to
visible user-defined exceptions.
rationale
No exception defined by an implementation can be guaranteed to be portable to
other implementations whether or not they are from the same vendor. Not only
may the names be different, but the range of conditions triggering the
exceptions may be different also.
If you create interface packages for the implementation-specific portions of
your program, those packages can catch or recognize implementation specific
exceptions and convert them into user-defined exceptions that have been
declared in the specification. Do not allow yourself to be forced to find and
change the name of every handler you have written for these exceptions when
the program is ported.
7.6 REPRESENTATION CLAUSES AND IMPLEMENTATION-
DEPENDENT FEATURES
Ada provides many implementation-dependent features that permit greater
control over and interaction with the underlying hardware architecture than is
normally provided by a high-order language. These mechanisms are intended to
assist in systems programming and real-time programming to obtain greater
efficiency (e.g., specific size layout of variables through representation
clauses) and direct hardware interaction (e.g., interrupt entries) without
having to resort to assembly level programming.
Given the objectives for these features, it is not surprising that you must
usually pay a significant price in portability to use them. In general, where
portability is the main objective, do not use these features. When you must
use these features, encapsulate them in packages well-commented as interfacing
to the particular target environment. This section identifies the various
features and their recommended use with respect to portability.
7.6.1 Representation Clauses
guideline
o Use algorithms that do not depend on the representation of the data
and therefore do not need representation clauses.
o Use representation clauses when accessing interface data or when a
specific representation is needed to implement a design.
rationale
In many cases, it is easy to use representation clauses to implement an
algorithm, even when it is not necessary. There is also a tendency to
document the original programmer's assumptions about the representation for
future reference. But there is no guarantee that another implementation will
support the representation chosen. Unnecessary representation clauses also
confuse porting or maintenance efforts which must assume that the programmer
depends on the documented representation.
Interfaces to non-Ada systems and external devices are the most common
situations where a representation clause is needed. Uses of pragma Interface
and address clauses should be evaluated during design and porting to determine
whether a representation clause is needed.
note
During a porting effort, all representation clauses can be evaluated as either
design artifacts or specifications for accessing interface data that might
change with a new implementation.
7.6.2 Package System
guideline
o Avoid using package System constants except in attempting to
generalize other machine dependent constructs.
rationale
Since the values in this package are implementation-provided, unexpected
effects can result from their use.
exceptions
Do use package System constants to parameterize other implementation-dependent
features (see Pappas [1985] w13.7.1).
7.6.3 Machine Code Inserts
guideline
o Avoid machine code inserts.
rationale
Appendix C (of Department of Defense 1983) suggests that the package
implementing machine code inserts is optional. Additionally, it is not
standardized so that machine code inserts are most likely not portable. In
fact, it is possible that two different vendors' syntax will differ for an
identical target and differences in lower-level details such as register
conventions will hinder portability.
exceptions
If machine code inserts must be used to meet another project requirement,
recognize the portability decreasing effects.
In the declarative region of the body of the routine where the machine code
insert is being used, insert comments explaining what function the insert
provides, and (especially) why the insert is necessary. Comment the necessity
of using machine code inserts by delineating what went wrong with attempts to
use other higher-level constructs.
7.6.4 Interfacing Foreign Languages
guideline
o Avoid interfacing Ada with other languages.
o Isolate all subprograms employing pragma Interface to
implementation-dependent (interface) package bodies.
rationale
The problems with employing pragma Interface are complex. These problems
include pragma syntax differences, conventions for linking/binding Ada to
other languages, and mapping Ada variables to foreign language variables. By
hiding these dependencies within interface packages, the amount of code
modification can be reduced.
exceptions
It is often necessary to interact with other languages, if only an assembly
language to reach certain hardware features. In these cases, clearly comment
the requirements and limitations of the interface and pragma Interface usage.
7.6.5 Implementation-Defined Pragmas and Attributes
guideline
o Avoid pragmas and attributes added by the compiler implementor.
rationale
The Ada Language Reference Manual (Department of Defense 1983) permits an
implementor to add pragmas and attributes to exploit a particular hardware
architecture or software environment. These are obviously even more
implementation-specific and therefore less portable than an implementor's
interpretations of the predefined pragmas and attributes.
exceptions
Some implementation-dependent features are gaining wide acceptance in the Ada
community to help alleviate inherent inefficiencies in some Ada features. A
good example of this is the "fast interrupt" mechanism that provides a minimal
interrupt latency time in exchange for a restrictive tasking environment. Ada
community groups (e.g., SIGAda's ARTEWG) are attempting to standardize a
common mechanism and syntax to provide this capability. By being aware of
industry trends when specialized features must be used, you can take a more
general approach that will help minimize the porting job.
7.6.6 Unchecked Deallocation
guideline
o Avoid dependence on Unchecked_Deallocation (see Guideline ).
rationale
The unchecked storage deallocation mechanism is one method for overriding the
default time at which allocated storage is reclaimed. The earliest default
time is when an object is no longer accessible, e.g., when control leaves the
scope where an access type was declared (the exact point after this time is
implementation- dependent). Any unchecked deallocation of storage performed
prior to this may result in an erroneous Ada program if an attempt is made to
access the object.
This guideline is stronger than Guideline because of the extreme dependence on
the implementation of Unchecked_Deallocation. Using it could cause
considerable difficulty with portability.
exceptions
Using unchecked deallocation of storage can be beneficial in local control of
highly iterative or recursive algorithms where available storage may be
exceeded.
7.6.7 Unchecked Conversion
guideline
o Avoid using Unchecked_Conversion (see Guideline ).
rationale
The unchecked type conversion mechanism is, in effect, a means of bypassing
the strong typing facilities in Ada. An implementation is free to limit the
types that may be matched and the results that occur when object sizes differ.
exceptions
Unchecked type conversion is useful in implementation dependent parts of Ada
programs (where lack of portability is isolated) where low-level programming
and foreign language interfacing is the objective.
7.6.8 Run Time Dependencies
guideline
o Avoid the direct invocation of or implicit dependence upon an
underlying host operating system or Ada run time support system.
rationale
Features of an implementation not specified in the Ada Language Reference
Manual (Department of Defense 1983) will usually differ between
implementations. Specific implementation-dependent features are not likely to
be provided in other implementations. Even if a majority of vendors eventually
provide similar features, they are unlikely to have identical formulations.
Indeed, different vendors may use the same formulation for (semantically)
entirely different features.
Try to avoid these when coding. Consider the consequences of including system
calls in a program on a host development system. If these calls are not
flagged for removal and replacement, the program could go through development
and testing only to be unusable when moved to a target environment which lacks
the facilities provided by those system calls on the host.
exceptions
In real-time embedded systems, making calls to low-level support system
facilities may often be unavoidable. Isolate the uses of these facilities may
be too difficult. Comment them as you would machine code inserts (see
Guideline ); they are, in a sense, instructions for the virtual machine
provided by the support system. When isolating the uses of these features,
provide an interface for the rest of your program to use which can be ported
through replacement of the interface's implementation.
7.7 INPUT/OUTPUT
Input/Output facilities in Ada are not a part of the syntactic definition of
the language. The constructs in the language have been used to define a set of
packages for this purpose. These packages are not expected to meet all the I/O
needs of all applications, in particular embedded systems. They serve as a
core subset that may be used on straightforward data, and that can be used as
examples of building I/O facilities upon the low-level constructs provided by
the language. Providing an I/O definition that could meet the requirements of
all applications and integrate with the many existing operating systems would
result in unacceptable implementation dependencies.
The types of portability problems encountered with I/O tend to be different
for applications running with a host operating system versus embedded targets
where the Ada run-time is self-sufficient. Interacting with a host operating
system offers the added complexity of coexisting with the host file system
structures (e.g., hierarchical directories), access methods (e.g., ISAM), and
naming conventions (e.g., logical names and aliases based on the current
directory). The section on Input/Output in ARTEWG (1986) provides some
examples of this type of dependency. Embedded applications have different
dependencies that often tie them to the low-level details of their hardware
devices.
The major defense against these inherent implementation dependencies in I/O is
to try to isolate their functionality in any given application. The majority
of the following guidelines are focused in this direction.
7.7.1 Name and Form Parameters
guideline
o Use constants and variables as symbolic actuals for the Name and Form
parameters on the predefined I/O packages. Declare and initialize them in an
implementation dependency package.
rationale
The format and allowable values of these parameters on the predefined I/O
packages can vary greatly between implementations. Isolation of these values
facilitates portability. Note that not specifying a Form string or using a
null value does not guarantee portability since the implementation is free to
specify defaults.
note
It may be desirable to further abstract the I/O facilities by defining
additional Create and Open procedures that hide the visibility of the Form
parameter entirely (see Pappas 1985, 54-55).
7.7.2 File Closing
guideline
o Close all files explicitly.
rationale
The Ada Language Reference Manual (Department of Defense 1983, w14.1) states,
"The language does not define what happens to external files after completion
of the main program (in particular, if corresponding files have not been
closed)." The possibilities range from being closed in an anticipated manner
to deletion.
The disposition of a closed temporary file may vary, perhaps affecting
performance and space availability (ARTEWG 1986).
7.7.3 I/O on Access Types
guideline
o Avoid performing I/O on access types.
rationale
The Ada Language Reference Manual (Department of Defense 1983) does not require
that it be supported. When such a value is written, it is placed out of reach
of the implementation. Thus, it is out of reach of the reliability-enhancing
controls of strong type checking.
Consider the meaning of this operation. One possible implementation of the
values of access types is virtual addresses. If you write such a value, how
can you expect another program to read that value and make any sensible use of
it? The value cannot be construed to refer to any meaningful location within
the reader's address space, nor can a reader infer any information about the
writer's address space from the value read. The latter is the same problem
that the writer would have trying to interpret or use the value if it is read
back in. To wit, a garbage collection and/or heap compaction scheme may have
moved the item formerly accessed by that value, leaving that value "pointing"
at space which is now being put to indeterminable uses by the underlying
implementation.
7.7.4 Package Low_Level_IO
guideline
o Minimize and isolate using the predefined package Low_Level_IO.
rationale
Low_Level_IO is intended to support direct interaction with physical devices
that are usually unique to a given host or target environment. In addition,
the data types provided to the procedures are implementation-defined. This
allows vendors to define different interfaces to an identical device.
exceptions
Those portions of an application that must deal with this level of I/O, e.g.,
device drivers and real-time components dealing with discretes, are inherently
nonportable. Where performance allows, structure these components to isolate
the hardware interface. Only within these isolated portions is it advantageous
to employ the Low_Level_IO interface which is portable in concept and general
procedural interface, if not completely so in syntax and semantics.
7.8 SUMMARY
fundamentals
o Make considered assumptions about the support provided for the
following on potential target platforms:
- Number of bits available for type Integer (range constraints).
- Number of decimal digits of precision available for floating point
types.
- Number of bits available for fixed-point types (delta and range
constraints).
- Number of characters per line of source text.
- Number of bits for Universal_Integer expressions.
- Number of seconds for the range of Duration.
- Number of milliseconds for Duration'Small.
o Don't implicitly use an implementation limit.
o Use highlighting comments for each package, subprogram and task where
any nonportable features are present.
o For each nonportable feature employed, describe the expectations for
that feature.
o Use only a parameterless procedure as the main program.
o Create packages specifically designed to isolate hardware and
implementation dependencies and designed so that their specification will not
change.
o Clearly indicate the objectives if machine or solution efficiency is
the reason for hardware or implementation dependent code.
o For the packages that hide implementation dependencies, maintain
different package bodies for different target environments.
o Isolate interrupt receiving tasks into implementation dependent
packages.
o Avoid the use of vendor supplied packages.
o Avoid the use of features added to the predefined packages that are
not specified in the language definition.
numeric types and expressions
o Do not use the predefined numeric types in package Standard. Use
range and digits declarations and let the implementation do the derivation
implicitly from the predefined types.
o For programs that require greater accuracy than that provided by the
global assumptions, define a package that declares a private type and
operations as needed; see Pappas (1985) for a full explanation and examples.
o Know the Ada model for floating point types and arithmetic.
o Carefully analyze what accuracy and precision you really need.
o Do not press the accuracy limits of the machine(s).
o Comment the analysis and derivation of the numerical aspects of a
program.
o Use named numbers or universal real expressions rather than constants
of any particular type.
o Anticipate values of subexpressions to avoid exceeding the range of
their type. Use derived types, subtypes, factoring, and range constraints on
numeric types as described in Guidelines , , and .
o Use <= and >= to do relational tests on real valued arguments,
avoiding the <, >, =, and /= operations.
o Use values of type attributes in comparisons and checking for small
values.
storage control
o Do not use a representation clause to specify number of storage units.
tasking
o Do not depend on the order in which task objects are activated when
declared in the same declarative list.
o Do not depend on a particular delay being achievable (Nissen and
Wallis 1984).
o Never use a busy waiting loop instead of a delay.
o Design to limit polling to those cases where it is absolutely
necessary.
o Never use knowledge of the execution pattern of tasks to achieve
timing requirements.
o Do not assume a correlation between System.Tick and package Calendar
or type Duration (see Guideline ).
o Do not depend on the order in which guard conditions are evaluated or
on the algorithm for choosing among several open select alternatives.
o Do not assume that tasks execute uninterrupted until they reach a
synchronization point.
o Use pragma Priority to distinguish general levels of importance only
(see Guideline ).
o Avoid using the abort statement.
o Do not share variables.
o Have tasks communicate through the rendezvous mechanism.
o Do not use shared variables as a task synchronization device.
o Use pragma Shared only when you are forced to by run time system
deficiencies.
exceptions
o Do not depend on the exact locations at which predefined exceptions
are raised.
o Catch Numeric_Error exceptions with a Numeric_Error | Constraint_Error
exception handler.
o Do not use a separate exception handler for Numeric_Error and
Constraint_Error.
o Do not raise implementation-defined exceptions.
o Convert implementation defined exceptions within interface packages to
visible user-defined exceptions.
representation clauses and implementation-dependent features
o Use algorithms that do not depend on the representation of the data
and therefore do not need representation clauses.
o Use representation clauses when accessing interface data or when a
specific representation is needed to implement a design.
o Avoid using package System constants except in attempting to
generalize other machine dependent constructs.
o Avoid machine code inserts.
o Avoid interfacing Ada with other languages.
o Isolate all subprograms employing pragma Interface to
implementation-dependent (interface) package bodies.
o Avoid pragmas and attributes added by the compiler implementor.
o Avoid dependence on Unchecked_Deallocation (see Guideline ).
o Avoid using Unchecked_Conversion (see Guideline ).
o Avoid the direct invocation of or implicit dependence upon an
underlying host operating system or Ada run time support system.
input/output
o Use constants and variables as symbolic actuals for the Name and Form
parameters on the predefined I/O packages. Declare and initialize them in an
implementation dependency package.
o Close all files explicitly.
o Avoid performing I/O on access types.
o Minimize and isolate using the predefined package Low_Level_IO.
CHAPTER 8
Reusability
Reusability is the extent to which code can be used in different applications
with minimal change. As code is reused in a new application, that new
application partially inherits the attributes of that code. If it is
maintainable, the application is more maintainable. If it is portable, then
the application is more portable. So this chapter's guidelines are most useful
when all of the other guidelines in this book are also applied.
Several guidelines are directed at the issue of maintainability. Maintainable
code is easy to change to meet new or changing requirements. Maintainability
plays a special role in reuse. When attempts are made to reuse code, it is
often necessary to change it to suit the new application. If the code cannot
be changed easily, it is less likely to be reused.
There are many issues involved in software reuse: whether to reuse parts, how
to store and retrieve reusable parts in a library, how to certify parts, how
to maximize the economic value of reuse, how to provide incentives to
engineers and entire companies to reuse parts rather than reinvent them, and
so on. This chapter ignores these managerial, economic, and logistic issues to
focus on the single technical issue of how to write software parts in Ada to
increase reuse potential. The other issues are just as important but are
outside of the scope of this book.
One of the design goals of Ada was to facilitate the creation and use of
reusable parts to improve productivity. To this end, Ada provides features to
develop reusable parts and to adapt them once they are available. Packages,
visibility control, and separate compilation support modularity and
information hiding (see Guidelines , , , and ). This allows the separation of
application-specific parts of the code, maximizes the general purpose parts
suitable for reuse, and allows the isolation of design decisions within
modules, facilitating change. The Ada type system supports localization of
data definitions so that consistent changes are easy to make. Generic units
directly support the development of general purpose, adaptable code that can
be instantiated to perform specific functions. Using these features carefully,
and in conformance to the guidelines in this book, produces code that is more
likely to be reusable.
Reusable code is developed in many ways. Code may be scavenged from a previous
project. A reusable library of code may be developed from scratch for a
particularly well understood domain: such as a math library. Reusable code may
be developed as an intentional byproduct of a specific application. Reusable
code may be developed a certain way because a design method requires it. These
guidelines are intended to apply in all of these situations.
The experienced programmer recognizes that software reuse is much more a
requirements and design issue than a coding issue. The guidelines in this
section are intended to work within an overall method for developing reusable
code. This section will not deal with artifacts of design, testing, etc. Some
research into reuse issues related specifically to the Ada language can be
found in AIRMICS (1990), Edwards (1990), and Wheeler (1992).
Regardless of development method, experience indicates that reusable code has
certain characteristics, and this chapter makes the following assumptions:
- Reusable parts must be understandable. A reusable part should be a
model of clarity. The requirements for commenting reusable parts are even more
stringent than those for parts specific to a particular application.
- Reusable parts must be of the highest possible quality. They must be
correct, reliable, and robust. An error or weakness in a reusable part may
have far-reaching consequences, and it is important that other programmers can
have a high degree of confidence in any parts offered for reuse.
- Reusable parts must be adaptable. To maximize its reuse potential, a
part must be able to adapt to the needs of a wide variety of users.
- Reusable parts should be independent. It should be possible to reuse a
single part without also adopting many other parts that are apparently
unrelated.
In addition to these criteria, a reusable part must be easier to reuse than to
reinvent, must be efficient, and must be portable. If it takes more effort to
reuse a part than to create one from scratch, or if the reused part is simply
not efficient enough, reuse does not occur as readily. For guidelines on
portability, see Chapter 7.
This chapter should not be read in isolation. In many respects, a
well-written, reusable component is simply an extreme example of a
well-written component. All of the guidelines in the previous chapters apply
to reusable components as well as components specific to a single application.
The guidelines listed here apply specifically to reusable components.
8.1 UNDERSTANDING AND CLARITY
It is particularly important that parts intended for reuse should be easy to
understand. The following must be immediately apparent from inspection of the
comments and the code itself: what the part does, how to use it, what
anticipated changes might be made to it in the future, and how it works. For
maximum readability of reusable parts, follow the guidelines in Chapter 3,
some of which are repeated more strongly below.
8.1.1 Application-Independent Naming
guideline
o Select the least restrictive names possible for reusable parts and
their identifiers.
o Select the generic name to avoid conflicting with the naming
conventions of instantiations of the generic.
o Use names which indicate the behavioral characteristics of the
reusable part, as well as its abstraction.
example
General-purpose stack abstraction:
------------------------------------------------------------------------
generic
type Item is limited private;
...
package Bounded_Stack is
procedure Push (New_Item : in Item);
procedure Pop (Newest_Item : in Item);
...
end Bounded_Stack;
------------------------------------------------------------------------
Renamed appropriately for use in current application:
------------------------------------------------------------------------
with Bounded_Stack;
package Cafeteria is
type Tray is limited private;
package Tray_Stack is new Bounded_Stack (Item => Tray , ...);
...
end Cafeteria;
------------------------------------------------------------------------
rationale
Choosing a general or application-independent name for a reusable part
encourages its wide reuse. When the part is used in a specific context, it can
be instantiated (if generic) or renamed with a more specific name.
When there is an obvious choice for the simplest, clearest name for a reusable
part, it is a good idea to leave that name for use by the reuser of the part,
choosing a longer, more descriptive name for the reusable part. Thus,
Bounded_Stack is a better name than Stack for a generic stack package because
it leaves the simpler name Stack available to be used by an instantiation.
Include indications of the behavioral characteristics (but not indications of
the implementation) in the name of a reusable part so that multiple parts with
the same abstraction (e.g., multiple stack packages) but with different
restrictions (bounded, unbounded, etc.) can be stored in the same Ada library
and used as part of the same Ada program.
8.1.2 Abbreviations
guideline
o Do not use any abbreviations in identifier or unit names.
example
------------------------------------------------------------------------
with Calendar;
package Greenwich_Mean_Time is
function Clock return Calendar.Time;
...
end Greenwich_Mean_Time;
------------------------------------------------------------------------
But the following abbreviation may not be clear when used in an application.
with Calendar;
with Greenwich_Mean_Time;
...
function Get_GMT return Calendar.Time renames
Greenwich_Mean_Time.Clock;
rationale
This is a stronger guideline than Guideline . However well commented, an
abbreviation may cause confusion in some future reuse context. Even
universally accepted abbreviations, such as GMT for Greenwich Mean Time, can
cause problems and should be used only with great caution.
The difference between this guideline and Guideline 3.1.4 involves issues of
domain. When the domain is well-defined, abbreviations and acronyms that are
accepted in that domain will clarify the meaning of the application. When
that same code is removed from its domain-specific context, those
abbreviations may become meaningless.
In the example above, the package, Greenwich_Mean_Time, could be used in any
application without loss of meaning. But the function Get_GMT could easily be
confused with some other acronym in a different domain.
note
See Guideline 5.7.2 concerning the proper use of the renames clause. If a
particular application makes extensive use of the Greenwich_Mean_Time domain,
it may be appropriate to rename the package, GMT, within that application:
with Greenwich_Mean_Time;
...
package GMT renames Greenwich_Mean_Time;
8.1.3 Generic Formal Parameters
guideline
o Document the expected behavior of generic formal parameters just as
any package specification is documented.
example
The following example shows how a very general algorithm can be developed, but
must be clearly documented to be used:
------------------------------------------------------------------------
generic
-- Index provides access to values in a structure. For example,
-- an array, A.
type Index is (<>);
-- The function, Less_Than, does NOT compare the indexes
-- themselves;it compares the elements of the structure:
-- Less_Than (i,j) <==> A(i) < A(j)
with function Less_Than (Index1 : in Index;
Index2 : in Index)
return Boolean;
-- This procedure swaps values of the structure (the mode won't
-- allow the indexes themselves to be swapped!):
-- Less_Than (i,j) and then Swap (i,j) ==> Less_Than (j,i).
with procedure Swap (Index1 : in Index;
Index2 : in Index);
-- After the call to Quick_Sort, the indexed structure will be
-- sorted:
-- For all i,j in First..Last : i<j => A(i) < A(j).
procedure Quick_Sort (First : in Index := Index'First;
Last : in Index := Index'Last);
------------------------------------------------------------------------
rationale
The generic capability is one of Ada's strongest features because of its
formalization. However, not all of the assumptions made about generic formal
parameters can be expressed directly in Ada. It is important that any user of
a generic know exactly what that generic needs in order to behave correctly.
In a sense, a generic specification is a contract where the instantiator must
supply the formal parameters and in return receives a working instance of the
specification. Both parties are best served when the contract is complete and
clear about all assumptions.
8.2 ROBUSTNESS
The following guidelines improve the robustness of Ada code. It is easy to
write code that depends on an assumption which you do not realize that you are
making. When such a part is reused in a different environment, it can break
unexpectedly. The guidelines below show some ways in which Ada code can be
made to automatically conform to its environment, and some ways in which it
can be made to check for violations of assumptions. Finally, some guidelines
are given to warn you about errors which Ada does not catch as soon as you
might like.
8.2.1 Named Numbers
guideline
o Use named numbers and static expressions to allow multiple
dependencies to be linked to a small number of symbols.
example
------------------------------------------------------------------------
procedure Disk_Driver is
-- In this procedure, a number of important disk parameters are
-- linked.
Number_Of_Sectors : constant := 4;
Number_Of_Tracks : constant := 200;
Number_Of_Surfaces : constant := 18;
Sector_Capacity : constant := 4_096;
Track_Capacity : constant := Number_Of_Sectors * Sector_Capacity;
Surface_Capacity : constant := Number_Of_Tracks * Track_Capacity;
Disk_Capacity : constant := Number_Of_Surfaces * Surface_Capacity;
type Sector_Range is range 1 .. Number_Of_Sectors;
type Track_Range is range 1 .. Number_Of_Tracks;
type Surface_Range is range 1 .. Number_Of_Surfaces;
type Track_Map is array (Sector_Range) of ...;
type Surface_Map is array (Track_Range) of Track_Map;
type Disk_Map is array (Surface_Range) of Surface_Map;
begin -- Disk_Driver
...
end Disk_Driver;
------------------------------------------------------------------------
rationale
To reuse software that uses named numbers and static expressions
appropriately, just one or a small number of constants need to be reset and
all declarations and associated code are changed automatically. Apart from
easing reuse, this reduces the number of opportunities for error and documents
the meanings of the types and constants without using error-prone comments.
8.2.2 Unconstrained Arrays
guideline
o Use unconstrained array types for array formal parameters and array
return values.
o Make the size of local variables depend on actual parameter size where
appropriate.
example
...
type Vector is array (Vector_Index range <>) of Element;
type Matrix is array
(Vector_Index range <>, Vector_Index range <>) of Element;
...
---------------------------------------------------------------------
procedure Matrix_Operation (Data : in Matrix) is
Workspace : Matrix (Data'Range(1), Data'Range(2));
Temp_Vector : Vector (Data'First(1) .. 2 * Data'Last(1));
...
---------------------------------------------------------------------
rationale
Unconstrained arrays can be declared with their sizes dependent on formal
parameter sizes. When used as local variables, their sizes change
automatically with the supplied actual parameters. This facility can be used
to assist in the adaption of a part since necessary size changes in local
variables are taken care of automatically.
8.2.3 Assumptions
guideline
o Minimize the number of assumptions made by a unit.
o For assumptions which cannot be avoided, use types to automatically
enforce conformance.
o For assumptions which cannot be automatically enforced by types, add
explicit checks to the code.
o Document all assumptions.
example
The following poorly written function documents, but does not check, its
assumption:
-- Assumption: BCD value is less than 4 digits.
function Binary_To_BCD (Binary_Value : in Natural)
return BCD;
The next example enforces conformance with its assumption, making the checking
automatic, and the comment unnecessary:
type Binary_Values is new Natural range 0 .. 9_999;
function Binary_To_BCD (Binary_Value : in Binary_Values)
return BCD;
The next example explicitly checks and documents its assumption:
---------------------------------------------------------------------
-- Out_Of_Range raised when BCD value exceeds 4 digits.
function Binary_To_BCD (Binary_Value : in Natural)
return BCD is
Maximum_Representable : constant Natural := 9_999;
begin -- Binary_To_BCD
if Binary_Value > Maximum_Representable then
raise Out_Of_Range;
end if;
...
end Binary_To_BCD;
---------------------------------------------------------------------
rationale
Any part that is intended to be used again in another program, especially if
the other program is likely to be written by other people, should be robust.
It should defend itself against misuse by defining its interface to enforce as
many assumptions as possible and by adding explicit defensive checks on
anything which cannot be enforced by the interface.
note
You can restrict the ranges of values of the inputs by careful selection or
construction of the types of the formal parameters. When you do so, the
compiler-generated checking code may be more efficient than any checks you
might write. Indeed, such checking is part of the intent of the strong typing
in the language. This presents a challenge, however, for generic units where
the user of your code selects the types of the parameters. Your code must be
constructed so as to deal with any value of any type the user may choose to
select for an instantiation.
8.2.4 Subtypes in Generic Specifications
guideline
o Beware of using subtypes as type marks when declaring generic formal
objects of type in out.
o Beware of using subtypes as type marks when declaring parameters or
return values of generic formal subprograms.
o Use attributes rather than literal values.
example
In the following example, it appears that any value supplied for the generic
formal object Object would be constrained to the range 1..10. It also appears
that parameters passed at run-time to the Put routine in any instantiation, and
values returned by the Get routine, would be similarly constrained.
subtype Range_1_10 is Integer range 1 .. 10;
---------------------------------------------------------------------
generic
Object : in out Range_1_10;
with procedure Put (Parameter : in Range_1_10);
with function Get return Range_1_10;
package Input_Output is
...
end Input_Output;
---------------------------------------------------------------------
However, this is not the case. Given the following legal instantiation:
subtype Range_15_30 is Integer range 15 .. 30;
Constrained_Object : Range_15_30 := 15;
procedure Constrained_Put (Parameter : in Range_15_30);
function Constrained_Get return Range_15_30;
package Constrained_Input_Output
is new Input_Output (Object => Constrained_Object,
Put => Constrained_Put,
Get => Constrained_Get);
...
Object, Parameter, and the return value of Get are constrained to the range
15..30. Thus, for example, if the body of the generic package contains an
assignment statement:
Object := 1;
Constraint_Error is raised when this instantiation is executed.
rationale
According to Sections 12.1.1(5) and 12.1.3(5) of the Ada Language Reference
Manual (Department of Defense 1983), when constraint checking is performed for
generic formal objects, and parameters and return values of generic formal
subprograms, the constraints of the actual subtype (not the formal subtype or
the base type) are enforced.
Thus, even with a generic unit which has been instantiated and tested many
times, and with an instantiation which reported no errors at instantiation
time, there can be a run-time error. Since the subtype constraints of the
generic formal are ignored, the Ada Language Reference Manual (Department of
Defense 1983) suggests using the name of a base type in such places to avoid
confusion. Even so, you must be careful not to assume the freedom to use any
value of the base type because the instantiation imposes the subtype
constraints of the generic actual parameter. To be safe, always refer to
specific values of the type via symbolic expressions containing attributes
like 'First, 'Last, 'Pred, and 'Succ rather than via literal values.
The best solution is to introduce a new generic formal type parameter and use
it in place of the subtype, as shown below:
------------------------------------------------------------------------
generic
type Object_Range is range <>;
Object : in out Object_Range;
with procedure Put (Parameter : in Object_Range);
with function Get return Object_Range;
package Input_Output is
...
end Input_Output;
------------------------------------------------------------------------
This is a clear statement by the developer of the generic unit that no
assumptions are made about the Objects type other than that it is an integer
type. This should reduce the likelihood of any invalid assumptions being made
in the body of the generic unit.
For generics, attributes provide the means to maintain generality. It is
possible to use literal values, but literals run the risk of violating some
constraint. For example, assuming an array's index starts at one may cause a
problem when the generic is instantiated for a zero-based array type.
8.2.5 Overloading in Generic Units
guideline
o Be careful about overloading the names of subprograms exported by the
same generic package.
example
------------------------------------------------------------------------
generic
type Item is limited private;
package Input_Output is
procedure Put (Value : in Integer);
procedure Put (Value : in Items);
end Input_Output;
------------------------------------------------------------------------
rationale
If the generic package shown in the example above is instantiated with Integer
(or any subtype of Integer) as the actual type corresponding to generic formal
Value, then the two Put procedures have identical interfaces, and all calls to
Put are ambiguous. Therefore, this package cannot be used with type Integer.
In such a case, it is better to give unambiguous names to all subprograms. See
Section 12.3(22) of the Ada Language Reference Manual (Department of Defense
1983) for more information.
8.2.6 Hidden Tasks
guideline
o Within a specification, document any tasks that would be activated by
with'ing the specification and by using any part of the specification.
o Document which generic formal parameters are accessed from a task
hidden inside the generic unit.
rationale
The effects of tasking becomes a major factor when reusable code enters the
domain of real-time systems. Even though tasks may be used for other
purposes, their effect on scheduling algorithms is still a concern and must be
clearly documented. With the task clearly documented, the real-time
programmer can then analyze performance, priorities, and so forth to meet
timing requirements; or if necessary, he can modify or even redesign the
component.
Concurrent access to data structures must be carefully planned to avoid
errors, especially for data structures which are not atomic (see Chapter 6 for
details). If a generic unit accesses one of its generic formal parameters
(reads or writes the value of a generic formal object or calls a generic
formal subprogram which reads or writes data) from within a task contained in
the generic unit, then there is the possibility of concurrent access for which
the user may not have planned. In such a case, the user should be warned by a
comment in the generic specification.
8.2.7 Exceptions
guideline
o Propagate exceptions out of reusable parts. Handle exceptions within
reusable parts only when you are certain that the handling is appropriate in
all circumstances.
o Propagate exceptions raised by generic formal subprograms after
performing any cleanup necessary to the correct operation of future
invocations of the generic instantiation.
o Leave state variables in a valid state when raising an exception.
o Leave parameters unmodified when raising an exception.
example
------------------------------------------------------------------------
generic
type Number is limited private;
with procedure Get (Value : out Number);
procedure Process_Numbers;
------------------------------------------------------------------------
procedure Process_Numbers is
Local : Number;
procedure Perform_Cleanup_Necessary_For_Process_Numbers is
separate;
...
begin -- Process_Numbers
...
Catch_Exceptions_Generated_By_Get:
begin
Get(Local);
exception
when others =>
Perform_Cleanup_Necessary_For_Process_Numbers;
raise;
end Catch_Exceptions_Generated_By_Get;
...
end Process_Numbers;
------------------------------------------------------------------------
rationale
On most occasions, an exception is raised because an undesired event (such as
floating-point overflow) has occurred. Such events often need to be dealt with
entirely differently with different uses of a particular software part. It is
very difficult to anticipate all the ways that users of the part may wish to
have the exceptions handled. Passing the exception out of the part is the
safest treatment.
In particular, when an exception is raised by a generic formal subprogram, the
generic unit is in no position to understand why or to know what corrective
action to take. Therefore, such exceptions should always be propagated back to
the caller of the generic instantiation. However, the generic unit must first
clean up after itself, restoring its internal data structures to a correct
state so that future calls may be made to it after the caller has dealt with
the current exception. For this reason, all calls to generic formal
subprograms should be within the scope of a when others exception handler if
the internal state is modified, as shown in the example above.
When a reusable part is invoked, the user of the part should be able to know
exactly what operation (at the appropriate level of abstraction) has been
performed. For this to be possible, a reusable part must always do all or none
of its specified function; it must never do half. Therefore, any reusable part
which terminates early by raising or propagating an exception should return to
the caller with no effect on the internal or external state. The easiest way
to do this is to test for all possible exceptional conditions before making
any state changes (modifying internal state variables, making calls to other
reusable parts to modify their states, updating files, etc.). When this is not
possible, it is best to restore all internal and external states to the values
which were current when the part was invoked before raising or propagating the
exception. Even when this is not possible, it is important to document this
potentially hazardous situation in the comment header of the specification of
the part.
A similar problem arises with parameters of mode out or in out when exceptions
are raised. The Ada language defines these modes in terms of "copy-in" and
"copy-back" semantics, but leaves the actual parameter-passing mechanism
undefined. When an exception is raised, the copy-back does not occur, but for
an Ada compiler which passes parameters by reference, the actual parameter has
already been updated. When parameters are passed by copy, the update does not
occur. To reduce ambiguity, increase portability, and avoid situations where
some but not all of the actual parameters are updated when an exception is
raised, it is best to treat values of out and in out parameters like state
variables, updating them only after it is certain that no exception will be
raised.
8.3 ADAPTABILITY
Reusable parts often need to be changed before they can be used in a specific
application. They should be structured so that change is easy and as localized
as possible. One way of achieving adaptability is to create general parts with
complete functionality, only a subset of which might be needed in a given
application. Another is to use Ada's generic construct to produce parts that
can be appropriately instantiated with different parameters. Both of these
approaches avoid the error-prone process of adapting a part by changing its
code, but have limitations and can carry some overhead.
Anticipated changes, that is, changes that can be reasonably foreseen by the
developer of the part, should be provided for as far as possible.
Unanticipated change can only be accommodated by carefully structuring a part
to be adaptable. Many of the considerations pertaining to maintainability
apply. If the code is of high quality, clear, and conforms to well-established
design principles such as information hiding, it is easier to adapt in
unforeseen ways.
8.3.1 Complete Functionality
guideline
o Provide complete functionality in a reusable part or set of parts.
Build in complete functionality, including end conditions, even if some
functionality is not needed in this application.
o More specifically, provide initialization and finalization procedures
for every data structure that may contain dynamic data.
example
Incoming : Queue;
...
Initialize(Incoming); -- initialization operation
...
if Is_Full(Incoming) then -- query operation
...
end if;
...
Finalize(Incoming); -- finalization operation
rationale
This is particularly important in designing/programming an abstraction.
Completeness ensures that you have configured the abstraction correctly,
without built-in assumptions about its execution environment. It also ensures
the proper separation of functions so that they are useful to the current
application and, in other combinations, to other applications. It is
particularly important that they be available to other applications; remember
that they can be "optimized" out of the final version of the current product.
When a reusable part can reasonably be implemented using dynamic data, then
any application that must control memory can use the initialization and
finalization routines to guard against memory leakage. Then if data
structures become dynamic, the applications that are sensitive to these
concerns can be easily adapted.
note
The example illustrates end condition functions. An abstraction should be
automatically initialized before its user gets a chance to damage it. When
that is not possible, it should be supplied with initialization operations. In
any case, it needs finalization operations. Where possible, query operations
should be provided to determine when limits are about to be exceeded, so that
the user can avoid causing exceptions to be raised.
It is also useful to provide reset operations for many objects. To see that a
reset and an initiation can be different, consider the analogous situation of
a "warm boot" and a "cold boot" on a personal computer.
Even if all of these operations are not appropriate for the abstraction, the
exercise of considering them aids in formulating a complete set of operations,
others of which may be used by another application.
Some implementations of the language link all subprograms of a package into
the executable file, ignoring whether they are used or not, making unused
operations a liability (see Guideline ). In such cases, where the overhead is
significant, create a copy of the fully functional part and comment out the
unused operations with an indication that they are redundant in this
application.
8.3.2 Generic Units
guideline
o Use generic units to avoid code duplication.
o Parameterize generic units for maximum adaptability.
o Reuse common instantiations of generic units, as well as the generic
units themselves.
rationale
Ada does not allow subprograms or data types to be passed as actual parameters
to subprograms during execution. Such parameters must be specified as generic
formal parameters to a generic unit when it is instantiated. Therefore, if you
want to write a subprogram for which there is variation from call to call in
the data type of objects on which it operates, or in the subprogram which it
calls, then you must write the subprogram as a generic unit and instantiate it
once for each combination of data type and subprogram parameters. The
instantiations of the unit can then be called as regular subprograms.
If you find yourself writing two very similar routines differing only in the
data type they operate on or the subprograms they call, then it is probably
better to write the routine once as a generic unit and instantiate it twice to
get the two versions you need. When the need arises later to modify the two
routines, the change only needs to be made in one place. This greatly
facilitates maintenance.
Once you have made such a choice, consider other aspects of the routine that
these two instances may have in common but which are not essential to the
nature of the routine. Factor these out as generic formal parameters. When the
need arises later for a third similar routine, it can be automatically
produced by a third instantiation, if you have foreseen all the differences
between it and the other two. A parameterized generic unit can be very
reusable.
It may seem that the effort involved in writing generic rather than nongeneric
units is substantial. However, making units generic is not much more difficult
or time-consuming than making them nongeneric once you become familiar with
the generic facilities. It is, for the most part, a matter of practice. Also,
any effort put into the development of the unit will be recouped when the unit
is reused, as it surely will be if it is placed in a reuse library with
sufficient visibility. Do not limit your thinking about potential reuse to the
application you are working on or to other applications with which you are
very familiar. Applications with which you are not familiar or future
applications might be able to reuse your software.
After writing a generic unit and placing it in your reuse library, the first
thing you are likely to do is to instantiate it once for your particular
needs. At this time, it is a good idea to consider whether there are
instantiations which are very likely to be widely used. If so, place each such
instantiation in your reuse library so that they can be found and shared by
others.
8.3.3 Using Generic Units to Encapsulate Algorithms
guideline
o Use generic units to encapsulate algorithms independently of data type.
example
This is the specification of a generic sort procedure:
------------------------------------------------------------------------
generic
type Element is limited private;
type Data is array (Positive range <>) of Element;
with function "<" (Left : in Element;
Right : in Element)
return Boolean is <>;
with procedure Swap (Left : in out Element;
Right : in out Element) is <>;
procedure Generic_Sort (Data_To_Sort : in out Data);
------------------------------------------------------------------------
The generic body looks just like a regular procedure body and can make full
use of the generic formal parameters in implementing the sort algorithm:
------------------------------------------------------------------------
procedure Generic_Sort (Data_To_Sort : in out Data) is
begin
...
for I in Data_To_Sort'Range loop
...
...
if Data_To_Sort(J) < Data_To_Sort(I) then
Swap(Data_To_Sort(I), Data_To_Sort(J));
end if;
...
...
end loop;
...
end Generic_Sort;
------------------------------------------------------------------------
The generic procedure can be instantiated as:
type Integer_Array is array (Positive range <>) of Integer;
procedure Swap (Left : in out Integer;
Right : in out Integer);
procedure Sort is
new Generic_Sort (Element => Integer,
Data => Integer_Array);
or
subtype String_80 is String (1 .. 80);
type String_Array is array (Positive range <>) of String_80;
procedure Swap (Left : in out String_80;
Right : in out String_80);
procedure Sort is
new Generic_Sort (Element => String_80,
Data => String_Array);
and called as:
Integer_Array_1 : Integer_Array (1 .. 100);
...
Sort(Integer_Array_1);
or
String_Array_1 : String_Array (1 .. 100);
...
Sort(String_Array_1);
rationale
A sort algorithm can be described independently of the data type being sorted.
This generic procedure takes the Element data type as a generic limited
private type parameter so that it assumes as little as possible about the data
type of the objects actually being operated on. It also takes Data as a
generic formal parameter so that instantiations can have entire arrays passed
to them for sorting. Finally, it explicitly requires the two operators that it
needs to do the sort: comparison and swap.The sort algorithm is encapsulated
without reference to any data type. The generic can be instantiated to sort an
array of any data type.
8.3.4 Using Generic Units for Abstract Data Types
guideline
o Use abstract data types in preference to abstract data objects.
o Use generic units to implement abstract data types independently of
their component data type.
example
This example presents a series of different techniques which can be used to
generate abstract data types and objects. A discussion of the merits of each
follows in the rationale section below. The first is an abstract data object
(ADO), also known as an abstract state machine (ASM). It encapsulates one
stack of integers.
------------------------------------------------------------------------
package Bounded_Stack is
subtype Element is Integer;
Maximum_Stack_Size : constant := 100;
procedure Push (New_Element : in Element);
procedure Pop (Top_Element : out Element);
Overflow : exception;
Underflow : exception;
...
end Bounded_Stack;
------------------------------------------------------------------------
The second is an abstract data type (ADT). It differs from the ADO by
exporting the Stacks type, which allows the user to declare any number of
stacks of integers. Note that since multiple stacks may now exist, it is
necessary to specify a Stack argument on calls to Push and Pop.
------------------------------------------------------------------------
package Bounded_Stack is
subtype Element is Integer;
type Stack is limited private;
Maximum_Stack_Size : constant := 100;
procedure Push (On_Top : in out Stack;
New_Element : in Element);
procedure Pop (From_Top : in out Stack;
Top_Element : out Element);
Overflow : exception;
Underflow : exception;
...
private
type Stack_Information;
type Stack is access Stack_Information;
end Bounded_Stack;
------------------------------------------------------------------------
The third is a parameterless generic abstract data object (GADO). It differs
from the ADO (the first example) simply by being generic, so that the user can
instantiate it multiple times to obtain multiple stacks of integers.
------------------------------------------------------------------------
generic
package Bounded_Stack is
subtype Element is Integer;
Maximum_Stack_Size : constant := 100;
procedure Push (New_Element : in Element);
procedure Pop (Top_Element : out Element);
Overflow : exception;
Underflow : exception;
...
end Bounded_Stack;
------------------------------------------------------------------------
The fourth is a slight variant on the third, still a generic abstract data
object (GADO) but with parameters. It differs from the third example by making
the data type of the stack a generic parameter so that stacks of data types
other than Integer can be created. Also, Max_Stack_Size has been made a
generic parameter which defaults to 100 but can be specified by the user,
rather than a constant defined by the package.
------------------------------------------------------------------------
generic
type Element is limited private;
with procedure Assign (From : in Element;
To : in out Element);
Maximum_Stack_Size : in Natural := 100;
package Bounded_Stack is
procedure Push (New_Element : in Element);
procedure Pop (Top_Element : in out Element);
Overflow : exception;
Underflow : exception;
...
end Bounded_Stack;
------------------------------------------------------------------------
Finally, the fifth is a generic abstract data type (GADT). It differs from the
GADO in the fourth example in the same way that the ADT in the second example
differed from the ADO in the first example; it exports the Stacks type, which
allows the user to declare any number of stacks.
------------------------------------------------------------------------
generic
type Element is limited private;
with procedure Assign (From : in Element;
To : in out Element);
Maximum_Stack_Size : in Natural := 100;
package Bounded_Stack is
type Stack is limited private;
procedure Push (On_Top : in out Stack;
New_Element : in Element);
procedure Pop (From_Top : in out Stack;
Top_Element : in out Element);
Overflow : exception;
Underflow : exception;
...
private
type Stack_Information;
type Stack is access Stack_Information;
end Bounded_Stack;
------------------------------------------------------------------------
rationale
The biggest advantage of an ADT over an ADO (or a GADT over a GADO) is that
the user of the package can declare as many objects as desired with an ADT.
These objects can be declared as standalone variables or as components of
arrays and records. They can also be passed as parameters. None of this is
possible with an ADO, where the single data object is encapsulated inside of
the package. Furthermore, an ADO provides no more protection of the data
structure than an ADT. When a private type is exported by the ADT package, as
in the example above, then for both the ADO and ADT, the only legal operations
which can modify the data are those defined explicitly by the package (in this
case, Push and Pop). For these reasons, an ADT or GADT is almost always
preferable to an ADO or GADO, respectively.
A GADO is similar to an ADT in one way: it allows multiple objects to be
created by the user. With an ADT, multiple objects can be declared using the
type defined by the ADT package. With a GADO (even a GADO with no generic
formal parameters, as shown in the third example), the package can be
instantiated multiple times to produce multiple objects. However, the
similarity ends there. The multiple objects produced by the instantiations
suffer from all restrictions described above for ADOs; they cannot be used in
arrays or records or passed as parameters. Furthermore, the objects are each
of a different type, and no operations are defined to operate on more than one
of them at a time. For example, there cannot be an operation to compare two
such objects or to assign one to another. The multiple objects declared using
the type defined by an ADT package suffer from no such restrictions; they can
be used in arrays and records and can be passed as parameters. Also, they are
all declared to be of the same type, so that it is possible for the ADT
package to provide operations to assign, compare, copy, etc. For these
reasons, an ADT is almost always preferable to a parameterless GADO.
The biggest advantage of a GADT or GADO over an ADT or ADO, respectively, is
that the GADT and GADO are generic and can thus be parameterized with types,
subprograms, and other configuration information. Thus, as shown above, a
single generic package can support bounded stacks of any data type and any
stack size, while the ADT and ADO above are restricted to stacks of Integer,
no more than 100 in size. For this reason, a GADO or GADT is almost always
preferable to an ADO or ADT.
The list of examples above is given in order of increasing power and
flexibility, starting with an ADO and ending with a GADT. These advantages are
not expensive in terms of complexity or development time. The specification of
the GADT above is not significantly harder to write or understand than the
specification of the ADO. The bodies are also nearly identical. Compare the
body for the simplest version, the ADO:
------------------------------------------------------------------------
package body Bounded_Stack is
type Stack_Slots is array (Natural range <>) of Element;
type Stack_Information is
record
Slots : Stack_Slots (1 .. Maximum_Stack_Size);
Index : Natural := 0;
end record;
Stack : Stack_Information;
---------------------------------------------------------------------
procedure Push (New_Element : in Element) is
begin
if Stack.Index >= Maximum_Stack_Size then
raise Overflow;
end if;
Stack.Index := Stack.Index + 1;
Stack.Slots(Stack.Index) := New_Element;
end Push;
---------------------------------------------------------------------
procedure Pop (Top_Element : out Element) is
begin
if Stack.Index <= 0 then
raise Underflow;
end if;
Top_Element := Stack.Slots(Stack.Index);
Stack.Index := Stack.Index - 1;
end Pop;
---------------------------------------------------------------------
...
end Bounded_Stack;
------------------------------------------------------------------------
with the body for the most powerful and flexible version, the GADT:
------------------------------------------------------------------------
package body Bounded_Stack is
type Stack_Slots is array (Natural range <>) of Element;
type Stack_Information is
record
Slots : Stack_Slots (1 .. Maximum_Stack_Size);
Index : Natural := 0;
end record;
---------------------------------------------------------------------
procedure Push (On_Top : in out Stack;
New_Element : in Element) is
begin
if On_Top.Index >= Maximum_Stack_Size then
raise Overflow;
end if;
On_Top.Index := On_Top.Index + 1;
Assign(From => New_Element,
To => On_Top.Slots(On_Top.Index));
end Push;
---------------------------------------------------------------------
procedure Pop (From_Top : in out Stack;
Top_Element : in out Element) is
begin
if From_Top.Index <= 0 then
raise Underflow;
end if;
Assign(From => From_Top.Slots(From_Top.Index),
To => Top_Element);
From_Top.Index := From_Top.Index - 1;
end Pop;
---------------------------------------------------------------------
...
end Bounded_Stack;
------------------------------------------------------------------------
There are only two differences. First, the ADO declares a local object called
Stack, while the GADT has one additional parameter (called Stack) on each of
the exported procedures Push and Pop. Second, the GADT uses the Assign
procedure rather than the assignment operator ":=" because the generic formal
type Element was declared limited private. This second difference could have
been avoided by declaring Element as private, but this is not recommended
because it reduces the composability of the generic reusable part.
note
The predefined simple types will need an assign or equality operation when
used to instantiate generics that expect limited private types. Although it
is a nuisance, it is simple enough for the few times it would apply.
8.3.5 Iterators
guideline
o Provide iterators for traversing complex data structures within
reusable parts.
o Provide both active and passive iterators.
o Protect the iterators from errors due to modification of the data
structure during iteration.
o Document the behavior of the iterators when the data structure is
modified during traversal.
example
The following package defines an abstract list data type, with both active and
passive iterators for traversing a list.
------------------------------------------------------------------------
generic
type Element is limited private;
...
package Unbounded_List is
type List is limited private;
procedure Insert (New_Element : in Element;
Into : in out List);
-- Passive (generic) iterator.
generic
with procedure Process (Each : in out Element);
procedure Iterate (Over : in List);
-- Active iterator
type Iterator is limited private;
procedure Initialize (Index : in out Iterator;
Existing_List : in List);
function More (Index : in Iterator)
return Boolean;
procedure Advance (Index : in out Iterator);
function Current (Index : in Iterator)
return Element;
procedure Finalize (Index : in out Iterator);
...
private
...
end Unbounded_List;
------------------------------------------------------------------------
After instantiating the generic package, and declaring a list, as:
------------------------------------------------------------------------
with Unbounded_List;
procedure List_User is
type Employee is ...;
package Roster is
new Unbounded_List (Element => Employee, ...);
Employee_List : Roster.List;
the passive iterator is instantiated, specifying the name of the routine which
should be called for each list element when the iterator is called.
---------------------------------------------------------------------
procedure Process_Employee (Each : in out Employee) is
begin
...
-- Perform the required action for EMPLOYEE here.
end Process_Employee;
---------------------------------------------------------------------
procedure Process_All is
new Roster.Iterate (Process => Process_Employee);
The passive iterator can then be called, as:
begin -- List_User
Process_All(Employee_List);
end List_User;
------------------------------------------------------------------------
Alternatively, the active iterator can be used, without the second
instantiation required by the passive iterator, as:
Iterator : Roster.Iterator;
procedure Process_Employee (Each : in Employee) is separate;
begin -- List_User
Roster.Initialize (Index => Iterator,
Existing_List => Employee_List);
while Roster.More(Iterator) loop
Process_Employee(Each => Roster.Current(Iterator));
Roster.Advance(Iterator);
end loop;
Roster.Finalize(Iterator);
end List_User;
------------------------------------------------------------------------
rationale
Iteration over complex data structures is often required and, if not provided
by the part itself, can be difficult to implement without violating
information hiding principles.
Active and passive iterators each have their advantages, but neither is
appropriate in all situations. Therefore, it is recommended that both be
provided to give the user a choice of which to use in each situation.
Passive iterators are simpler and less error-prone than active iterators, in
the same way that the for loop is simpler and less error-prone than the while
loop. There are fewer mistakes that the user can make in using a passive
iterator. Simply instantiate it with the routine to be executed for each list
element, and call the instantiation for the desired list. Active iterators
require more care by the user. The iterator must be declared, then initialized
with the desired list, then Current and Advance must be called in a loop until
More returns false, then the iterator must be terminated. Care must be taken
to perform these steps in the proper sequence. Care must also be taken to
associate the proper iterator variable with the proper list variable. It is
possible for a change made to the software during maintenance to introduce an
error, perhaps an infinite loop.
On the other hand, active iterators are more flexible than passive iterators.
With a passive iterator, it is difficult to perform multiple, concurrent,
synchronized iterations. For example, it is much easier to use active
iterators to iterate over two sorted lists, merging them into a third sorted
list. Also, for multidimensional data structures, a small number of active
iterator routines may be able to replace a large number of passive iterators,
each of which implements one combination of the active iterators. Consider,
for example, a binary tree. In what order should the passive iterator visit
the nodes? Depth first? Breadth first? What about the need to do a binary
search of the tree? Each of these could be implemented as a passive iterator,
but it may make more sense to simply define the More_Left, More_Right,
Advance_Left, and Advance_Right routines required by the active iterator to
cover all combinations.Finally, active iterators can be passed as generic
formal parameters while passive iterators cannot because passive iterators are
themselves generic, and generic units cannot be passed as parameters to other
generic units.
For either type of iterator, semantic questions can arise about what happens
when the data structure is modified as it is being iterated. When writing an
iterator, be sure to consider this possibility, and indicate with comments the
behavior which occurs in such a case. It is not always obvious to the user
what to expect. For example, to determine the "closure" of a mathematical
"set" with respect to some operation, a common algorithm is to iterate over
the members of the set, generating new elements and adding them to the set. In
such a case, it is important that elements added to the set during the
iteration be encountered subsequently during the iteration. On the other hand,
for other algorithms it may be important that the set which it iterated is the
set as it existed at the beginning of the iteration. In the case of a
prioritized list data structure, if the list is iterated in priority order, it
may be important that elements inserted at lower priority than the current
element during iteration not be encountered subsequently during the iteration,
but that elements inserted at a higher priority should be encountered. In any
case, make a conscious decision about how the iterator should operate, and
document that behavior in the package specification.
Deletions from the data structure also pose a problem for iterators. It is a
common mistake for a user to iterate over a data structure, deleting it piece
by piece during the iteration. If the iterator is not prepared for such a
situation, it is possible to end up dereferencing a null pointer or committing
a similar error. Such situations can be prevented by storing extra information
with each data structure which indicates whether it is currently being
iterated, and using this information to disallow any modifications to the data
structure during iteration. When the data structure is declared as a limited
private type, as should usually be the case when iterators are involved, the
only operations defined on the type are declared explicitly in the package
which declares the type, making it possible to add such tests to all
modification operations.
note
For further discussion of passive and active iterators see Ross (1989) and
Booch (1987).
8.3.6 Private and Limited Private Types
guideline
o Use limited private (not private) for generic formal types, explicitly
importing assignment and equality operations if required.
o Export the least restrictive type that maintains the integrity of the
data and abstraction while allowing alternate implementations.
o Use mode in out rather than out for parameters of a generic formal
subprogram, when the parameters are of an imported limited type.
example
The first example violates the guideline by having private (nonlimited)
generic formal types.
------------------------------------------------------------------------
generic
type Item is private;
type Key is private;
with function Key_Of (Current : in Item) return Key;
package List_Manager is
type List is limited private;
procedure Insert (Into : in List;
New_Item : in Item);
procedure Retrieve (From : in List;
Using : in Key;
Match : in out Item);
private
type List is ...
end List_Manager;
------------------------------------------------------------------------
The second example is improved by using limited private generic formal types
and importing the assignment operation for Item and the equality operator for
Key.
------------------------------------------------------------------------
generic
type Item is limited private;
type Key is limited private;
with procedure Assign (From : in Item;
To : in out Item);
with function "=" (Left : in Key;
Right : in Key)
return Boolean;
with function Key_Of (Current : in Item)
return Key;
package List_Manager is
type List is limited private;
procedure Insert (Into : in List;
New_Item : in Item);
procedure Retrieve (From : in List;
Using : in Key;
Match : in out Item);
private
type List is ...
end List_Manager;
------------------------------------------------------------------------
rationale
For a generic component to be usable in as many contexts as possible, it
should minimize the assumptions that it makes about its environment and should
make explicit any assumptions that are necessary. In Ada, the assumptions made
by generic units can be stated explicitly by the types of the generic formal
parameters. A limited private generic formal type prevents the generic unit
from making any assumptions about the structure of objects of the type or
about operations defined for such objects. A private (nonlimited) generic
formal type allows the assumption that assignment and equality comparison
operations are defined for the type. Thus, a limited private data type cannot
be specified as the actual parameter for a private generic formal type.
Therefore, generic formal types should almost always be limited private rather
than just private. This restricts the operations available on the imported
type within the generic unit body but provides maximum flexibility for the
user of the generic unit. Any operations required by the generic body should
be explicitly imported as generic formal subprograms. In the second example
above, only the operations required for managing a list of items with keys are
imported: Assign provides the ability to store items in the list, and Key_Of
and "=" support determination and comparison of keys during retrieval
operations. No other operations are required to manage the list. Specifically,
there is no need to be able to assign keys or compare entire items for
equality. Those operations would have been implicitly available if a private
type had been used for the generic formal type, and any actual type for which
they were not defined could not have been used with this generic unit.
The situation is reversed for types exported by a reusable part. For exported
types, the restrictions specified by limited and limited private are
restrictions on the user of the part, not on the part itself. To provide
maximum capability to the user of a reusable part, export types with as few
restrictions as possible. Apply restrictions as necessary to protect the
integrity of the exported data structures and the abstraction for the various
implementations envisioned for that generic.
In the example above, the List type is exported as limited private to hide the
details of the list implementation and protect the structure of a list.
Limited private is chosen over private to prevent the user from being able to
use the predefined assignment operation. This is important if the list is
implemented as an access type pointing to a linked lists of records, because
the predefined assignment would make copies of the pointer, not copies of the
entire list, which the user may not realize. If it is expected that the user
needs the ability to copy lists, then a copy operation should be explicitly
exported.
Because they are so restrictive, limited private types are not always the best
choice for types exported by a reusable part. In a case where it makes sense
to allow the user to make copies of and compare data objects, and when the
underlying data type does not involve access types (so that the entire data
structure gets copied or compared), then it is better to export a (nonlimited)
private type. In cases where it does not detract from the abstraction to
reveal even more about the type, then a nonprivate type (e.g., a numeric,
enumerated, record, or array type) should be used.
For cases where limited private types are exported, the package should
explicitly provide equality and assignment operations, if appropriate to the
abstraction. Limited private is almost always appropriate for types
implemented as access types. In such cases, predefined equality is seldom the
most desirable semantics. In such cases, also consider providing both forms of
assignment (assignment of a reference and assignment of a copy).
When the parameters are of an imported limited type, using mode in out instead
of out for parameters of a generic formal subprogram is important for the
following reason. Ada allows an out mode parameter of a limited private type
on a subprogram only when the subprogram is declared in the visible part of
the package that declares the private type. See Section 7.4.4(4) of the Ada
Language Reference Manual (Department of Defense 1983).There is no such
restriction in parameters of mode in out.The result of this is that if you
define a generic with a limited generic formal type and a generic formal
subprogram with an out parameter of that type, then the generic can only be
instantiated with a limited private actual type if the package which declares
that type also declares a subprogram with exactly the same profile (number and
types or arguments and return value) as your generic formal subprogram. A
potential user who wants to instantiate your generic with a limited type
defined in another package will not be able to write a subprogram to pass as
the generic actual.
note
It is possible (but clumsy) to redefine equality for nonlimited types.
However, if a generic imports a (nonlimited) private type and uses equality,
it will automatically use the predefined equality and not the user-supplied
redefinition. This is another argument for using limited private generic
formal parameters.
It should also be noted that the predefined packages, Sequential_IO and
Direct_IO, take private types. This will complicate IO requirements for
limited private types and should be considered during design.
8.4 INDEPENDENCE
A reusable part should be as independent as possible from other reusable
parts. A potential user is less inclined to reuse a part if that part
requires the use of other parts which seem unnecessary. The "extra baggage" of
the other parts wastes time and space. A user would like to be able to reuse
only that part which is perceived as useful.
Note that the concept of a "part" is intentionally vague here. A single
package does not need to be independent of each other package in a reuse
library, if the "parts" from that library which are typically reused are
entire subsystems. If the entire subsystem is perceived as providing a useful
function, the entire subsystem is reused. However, the subsystem should not be
tightly coupled to all the other subsystems in the reuse library, so that it
is difficult or impossible to reuse the subsystem without reusing the entire
library. Coupling between reusable parts should only occur when it provides a
strong benefit perceptible to the user.
8.4.1 Using Generic Parameters to Reduce Coupling
guideline
o Minimize with clauses on reusable parts, especially on their
specifications.
o Use generic parameters instead of with statements to reduce the number
of context clauses on a reusable part.
o Use generic parameters instead of with statements to import portions
of a package rather than the entire package.
example
A procedure like the following:
------------------------------------------------------------------------
with Package_A;
procedure Produce_And_Store_A is
...
begin -- Produce_And_Store_A
...
Package_A.Produce (...);
...
Package_A.Store (...);
...
end Produce_And_Store_A;
------------------------------------------------------------------------
can be rewritten as a generic unit:
------------------------------------------------------------------------
generic
with procedure Produce (...);
with procedure Store (...);
procedure Produce_And_Store;
------------------------------------------------------------------------
procedure Produce_And_Store is
...
begin -- Produce_And_Store
...
Produce (...);
...
Store (...);
...
end Produce_And_Store;
------------------------------------------------------------------------
and then instantiated:
------------------------------------------------------------------------
with Package_A;
with Produce_And_Store;
procedure Produce_And_Store_A is
new Produce_And_Store (Produce => Package_A.Produce,
Store => Package_A.Store);
------------------------------------------------------------------------
rationale
Context (with) clauses specify the names of other units upon which this unit
depends. Such dependencies cannot and should not be entirely avoided, but it
is a good idea to minimize the number of them which occur in the specification
of a unit. Try to move them to the body, leaving the specification independent
of other units so that it is easier to understand in isolation. Also, organize
your reusable parts in such a way that the bodies of the units do not contain
large numbers of dependencies on each other. Partitioning your library into
independent functional areas with no dependencies spanning the boundaries of
the areas is a good way to start. Finally, reduce dependencies by using
generic formal parameters instead of with statements, as shown in the example
above. If the units in a library are too tightly coupled, then no single part
can be reused without reusing most or all of the library.
The first (nongeneric) version of Produce_And_Store_A above is difficult to
reuse because it depends on Package_A which may not be general purpose or
generally available. If the operation Produce_And_Store has reuse potential
which is reduced by this dependency, a generic unit and an instantiation
should be produced as shown above. Note that the with clause for Package_A has
been moved from the Produce_And_Store generic procedure which encapsulates the
reusable algorithm to the Produce_And_Store_A instantiation. Instead of naming
the package which provides the required operations, the generic unit simply
lists the required operations themselves. This increases the independence and
reusability of the generic unit.
This use of generic formal parameters in place of with clauses also allows
visibility at a finer granularity. The with clause on the nongeneric version
of Produce_And_Store_A makes all of the contents of Package_A visible to
Produce_And_Store_A, while the generic parameters on the generic version make
only the Produce and Store operations available to the generic instantiation.
8.4.2 Coupling Due to Pragmas
guideline
o For nongenerics named in a context clause, avoid pragma Elaborate.
o Use pragma Elaborate for generics named in a context clause.
o Avoid pragma Priority in tasks hidden in reusable parts.
example
------------------------------------------------------------------------
generic
...
package Stack is
...
end Stack;
------------------------------------------------------------------------
with Stack;
pragma Elaborate (Stack); -- in case the body is not yet elaborated
package My_Stack is
new Stack (...);
------------------------------------------------------------------------
package body Stack is
begin
...
end Stack;
------------------------------------------------------------------------
rationale
Pragma Elaborate controls the order of elaboration of one unit with respect to
another. This is another way of coupling units and should be avoided when
possible in reusable parts, because it restricts the number of configurations
in which the reusable parts can be combined.
However, as more compilers begin to allow generics to be instantiated before
the bodies are compiled, elaboration orders that generally follow compilation
order may result in program errors. By forcing the compiler to elaborate the
generic before the instantiation, this error can be avoided or possibly
identify a problem of circularity (see 10.5 of Department of Defense 1983).
Pragma Priority controls the priority of a task relative to all other tasks in
a particular system. It is inappropriate in a reusable part which does not
know anything about the requirements and importance of other parts of the
systems in which it is reused. Give careful consideration to a reusable part
which claims that it can only be reused if its embedded task has the highest
priority in the system. No two such parts can ever be used together.
note
It is not possible to parameterize tasks with a priority to be specified at
instantiation or elaboration. However, a library of reusable parts that
contain tasks can be designed to depend on a single package of named numbers.
These named numbers can then be easily updated to fit the application's need
with the simple procedure of recompiling any library units that depend on the
named numbers. The configuration management implications of such an approach
are heavily dependent on the Ada development environment and compilation
system.
8.4.3 Part Families
guideline
o Create families of generic or other parts with similar specifications.
example
The Booch parts (Booch 1987) are an example of the application of this
guideline.
rationale
Different versions of similar parts (e.g., bounded versus unbounded stacks)
may be needed for different applications or to change the properties of a
given application. Often, the different behaviors required by these versions
cannot be obtained using generic parameters. Providing a family of parts with
similar specifications makes it easy for the programmer to select the
appropriate one for the current application or to substitute a different one
if the needs of the application change.
note
A reusable part which is structured from subparts which are members of part
families is particularly easy to tailor to the needs of a given application by
substitution of family members.
8.4.4 Conditional Compilation
guideline
o Structure reusable code to take advantage of dead code removal by the
compiler.
example
------------------------------------------------------------------------
package Matrix_Math is
...
type Algorithm is (Gaussian, Pivoting, Choleski, Tri_Diagonal);
generic
Which_Algorithm : in Algorithm := Gaussian;
procedure Invert ( ... );
end Matrix_Math;
------------------------------------------------------------------------
package body Matrix_Math is
...
---------------------------------------------------------------------
procedure Invert ( ... ) is
...
begin -- Invert
case Which_Algorithm is
when Gaussian => ... ;
when Pivoting => ... ;
when Choleski => ... ;
when Tri_Diagonal => ... ;
end case;
end Invert;
---------------------------------------------------------------------
end Matrix_Math;
------------------------------------------------------------------------
rationale
Some compilers omit object code corresponding to parts of the program which
they detect can never be executed. Constant expressions in conditional
statements take advantage of this feature where it is available, providing a
limited form of conditional compilation. When a part is reused in an
implementation that does not support this form of conditional compilation,
this practice produces a clean structure which is easy to adapt by deleting or
commenting out redundant code where it creates an unacceptable overhead.
This feature should be used when other factors prevent the code from being
separated into separate subunits. In the above example, it would be
preferable to have a different procedure for each algorithm. But the
algorithms may differ in slight but complex ways so as to make separate
procedures difficult to maintain.
caution
Be aware of whether your implementation supports dead code removal, and be
prepared to take other steps to eliminate the overhead of redundant code if
necessary.
8.4.5 Table-Driven Programming
guideline
o Write table-driven reusable parts where possible and appropriate.
example
The epitome of table-driven reusable software is a parser generation system. A
specification of the form of the input data and of its output, along with some
specialization code, is converted to tables that are to be "walked" by
pre-existing code using predetermined algorithms in the parser produced. Other
forms of "application generators" work similarly.
rationale
Table-driven (sometimes known as data-driven) programs have behavior that
depends on data with'ed at compile time or read from a file at run-time. In
appropriate circumstances, table-driven programming provides a very powerful
way of creating general-purpose, easily tailorable, reusable parts.
note
Consider whether differences in the behavior of a general-purpose part could be
defined by some data structure at compile- or run-time and, if so, structure
the part to be table-driven. The approach is most likely to be applicable when
a part is designed for use in a particular application domain but needs to be
specialized for use in a specific application within the domain. Take
particular care in commenting the structure of the data needed to drive the
part.
8.5 SUMMARY
understanding and clarity
o Select the least restrictive names possible for reusable parts and
their identifiers.
o Select the generic name to avoid conflicting with the naming
conventions of instantiations of the generic.
o Use names which indicate the behavioral characteristics of the
reusable part, as well as its abstraction.
o Do not use any abbreviations in identifier or unit names.
o Document the expected behavior of generic formal parameters just as
any package specification is documented.
robustness
o Use named numbers and static expressions to allow multiple
dependencies to be linked to a small number of symbols.
o Use unconstrained array types for array formal parameters and array
return values.
o Make the size of local variables depend on actual parameter size where
appropriate.
o Minimize the number of assumptions made by a unit.
o For assumptions which cannot be avoided, use types to automatically
enforce conformance.
o For assumptions which cannot be automatically enforced by types, add
explicit checks to the code.
o Document all assumptions.
o Beware of using subtypes as type marks when declaring generic formal
objects of type in out.
o Beware of using subtypes as type marks when declaring parameters or
return values of generic formal subprograms.
o Use attributes rather than literal values.
o Be careful about overloading the names of subprograms exported by the
same generic package.
o Within a specification, document any tasks that would be activated by
with'ing the specification and by using any part of the specification.
o Document which generic formal parameters are accessed from a task
hidden inside the generic unit.
o Propagate exceptions out of reusable parts. Handle exceptions within
reusable parts only when you are certain that the handling is appropriate in
all circumstances.
o Propagate exceptions raised by generic formal subprograms after
performing any cleanup necessary to the correct operation of future
invocations of the generic instantiation.
o Leave state variables in a valid state when raising an exception.
o Leave parameters unmodified when raising an exception.
adaptability
o Provide complete functionality in a reusable part or set of parts.
Build in complete functionality, including end conditions, even if some
functionality is not needed in this application.
o More specifically, provide initialization and finalization procedures
for every data structure that may contain dynamic data.
o Use generic units to avoid code duplication.
o Parameterize generic units for maximum adaptability.
o Reuse common instantiations of generic units, as well as the generic
units themselves.
o Use generic units to encapsulate algorithms independently of data type.
o Use abstract data types in preference to abstract data objects.
o Use generic units to implement abstract data types independently of
their component data type.
o Provide iterators for traversing complex data structures within
reusable parts.
o Provide both active and passive iterators.
o Protect the iterators from errors due to modification of the data
structure during iteration.
o Document the behavior of the iterators when the data structure is
modified during traversal.
o Use limited private (not private) for generic formal types, explicitly
importing assignment and equality operations if required.
o Export the least restrictive type that maintains the integrity of the
data and abstraction while allowing alternate implementations.
o Use mode in out rather than out for parameters of a generic formal
subprogram, when the parameters are of an imported limited type.
independence
o Minimize with clauses on reusable parts, especially on their
specifications.
o Use generic parameters instead of with statements to reduce the number
of context clauses on a reusable part.
o Use generic parameters instead of with statements to import portions
of a package rather than the entire package.
o For nongenerics named in a context clause, avoid pragma Elaborate.
o Use a pragma Elaborate for generics named in a context clause.
o Avoid pragma Priority in tasks hidden in reusable parts.
o Create families of generic or other parts with similar specifications.
o Structure reusable code to take advantage of dead code removal by the
compiler.
o Write table-driven reusable parts where possible and appropriate.
CHAPTER 9
Performance
In many ways, performance is at odds with maintainability and portability. To
achieve improved speed or memory usage, the most clear algorithm sometimes
gives way to confusing code. To exploit special purpose hardware or operating
system services, nonportable implementation dependencies are introduced. When
concerned about performance, you must decide how well each algorithm meets its
performance and maintainability requirements.
Apply these guidelines after an application is working correctly. Don't become
obsessed with improving performance when the application already meets its
performance requirements. Modifying code to improve performance can introduce
errors--so the benefits of tweaking an algorithm should be real and clearly
outweigh the risk.
These guidelines should be applied after you know your compiler and target
environment. Benchmarks and compiler generated assembly code can be evaluated
to help instantiate these guidelines for your development environment.
9.1 IMPROVING EXECUTION SPEED
9.1.1 Pragma Inline
guideline
o Use pragma Inline when calling overhead is a significant portion of
the routine's execution time.
example
procedure Assign (Variable : in out Integer;
Value : in Integer);
pragma Inline (Assign);
...
procedure Assign (Variable : in out Integer;
Value : in Integer) is
begin
Variable := Value;
end Assign;
rationale
Procedure and function invocations include overhead that is unnecessary when
the code involved is very small. These small routines are usually written to
maintain the implementation hiding characteristics of a package. They may
also simply pass their parameters unchanged to another routine. When one of
these routines appears in some code that needs to run faster, either the
implementation hiding principle needs to be violated or a pragma Inline can be
introduced.
The use of pragma Inline does have its disadvantages. It can create
compilation dependencies on the body; i.e., when the specification uses a
pragma Inline, both the specification and corresponding body may need to be
compiled before the specification can be used. As updates are made to the
code, a routine may become more complex (larger) and the continued use of a
pragma Inline may no longer be justified.
exception
Although it is rare, Inlining code may increase code size which can lead to
slower performance caused by additional paging. A pragma Inline may actually
thwart a compiler's attempt to use some other optimization technique such as
register optimization.
When a compiler is already doing a good job of selecting routines to be
inlined, the pragma may accomplish little, if any, improvement in execution
speed.
9.1.2 Blocks
guideline
o Use blocks to introduce late initialization (Guideline 5.6.9).
o Remove blocks that introduce overhead.
example
...
Initial : Matrix;
begin -- Find_Solution
Initialize_Solution_Matrix:
for Row in Initial'Range(1) loop
for Col in Initial'Range(2) loop
Initial(Row, Col) := Get_Value(Row, Col);
end loop;
end loop Initialize_Solution_Matrix;
Converge_To_The_Solution:
declare
Solution : Matrix := Identity;
Min_Iterations : constant Natural := ...;
begin -- Converge_To_The_Solution
for Iterations in 1 .. Min_Iterations loop
Converge(Solution, Initial);
end loop;
end Converge_To_The_Solution;
...
end Find_Solution;
rationale
Late initialization allows a compiler more choices in register usage
optimization. Depending on the circumstance, this may introduce a significant
performance improvement.
Some compilers incur a performance penalty when declarative blocks are
introduced. Careful analysis and timing tests by the programmer may identify
those declarative blocks that should be removed.
note
It is difficult to accurately predict through code inspections which
declarative blocks improve performance and which degrade performance.
However, with these general guidelines and a familiarity with the particular
implementation, performance can be improved.
9.1.3 Arrays
guideline
o Use constrained arrays.
o Use zero based indexing for arrays.
example
-- M, N are variables which change value at runtime.
type Unconstrained is array (Integer range M .. N) of Element;
type Zero_Based is array (Integer range 0 .. N - M) of Element;
type Constrained_0_Based is array (Integer range 0 .. 9) of Element;
rationale
Unconstrained arrays often leave address and offset calculations until
runtime. Constrained arrays can be optimized by performing some calculations
once at compile time. A detailed discussion of the tradeoffs and alternatives
can be found in NASA (1992).
Although zero based indexing is not as intuitive for humans, it simplifies
many of the necessary calculations for indexing into arrays.
note
Generic utilities for handling arrays can be instantiated on constrained or
unconstrained arrays with arbitrary indexes. Then the compiler can optimize
the utility when the more efficient structure is used (assuming the generic is
not sharing code!). Again, further details can be found in NASA (1992).
9.1.4 Mod and Rem Operators
guideline
o Use incremental schemes instead of the mod and rem operators when
possible.
example
The following is slow:
for I in 0 .. N loop
Update(Arr(I mod Modulator));
end loop;
The following is equivalent, and avoids the mod operator:
J := 0;
for I in 0 .. N loop
Update(Arr(J));
if J = Modulator then
J := 0;
else -- j < Modulator
J := J + 1;
end if;
end loop;
rationale
The mod and rem operators are very convenient, but relatively slow. In
isolated cases where performance is of concern, a straightforward mapping to
incremental schemes is possible.
note
Most of the incremental schemes that avoid the mod and rem operations are
prime candidates for generic utilities. Programmers may then conveniently
apply this optimization when needed.
9.1.5 Constraint Checking
guideline
o Use strong typing with carefully selected constraints to reduce
runtime constraint checking.
example
In this example, two potential constraint checks are eliminated: If the
function, Get_Response, returns String, then the initialization of the
variable, Input, would require constraint checking. If the variable, Last, is
type Positive, then the assignment inside the loop would require constraint
checking.
...
subtype Name_Index is Positive range 1 .. 32;
subtype Name is String (Name_Index);
...
function Get_Response return Name is separate;
...
begin
...
Find_Last_Period:
declare
-- No Constraint Checking needed for initialization
Input : constant Name := Get_Response;
Last_Period : Name_Index := 1;
begin -- Find_Last_Period
for I in Input'Range loop
if Input(I) = '.' then
-- No Constraint Checking needed in this `tight' loop
Last_Period := I;
end if;
end loop;
...
rationale
Since runtime constraint checking is associated with slow performance, it is
not intuitive that the addition of constrained types could actually improve
performance. However, the need for constraint checking appears in many places
regardless of the use of constrained subtypes. Even assignments to variables
that use the predefined types may need constraint checks. By consistently
using constrained types, many of the unnecessary runtime checking can be
eliminated. Instead, the checking is usually moved to less frequently
executed code involved in system input. In the example, the function,
Get_Response, may need to check the length of a user supplied string and raise
an exception.
Some compilers can do additional optimizations based on the information
provided by constrained types. For example, although an unconstrained array
does not have a fixed size, it has a maximum size which can be determined from
the range of its index. Performance can be improved by limiting this maximum
size to a "reasonable" number. Refer to the discussion on unconstrained arrays
found in NASA (1992).
9.2 SUMMARY
improving execution speed
o Use pragma Inline when calling overhead is a significant portion of
the routine's execution time.
o Use blocks to introduce late initialization (Guideline 5.6.9).
o Remove blocks that introduce overhead.
o Use constrained arrays.
o Use zero based indexing for arrays.
o Use incremental schemes instead of the mod and rem operators when
possible.
o Use strong typing with carefully selected constraints to reduce
runtime constraint checking.
CHAPTER 10
Complete Examples
This chapter contains example programs illustrating the use of
guidelines. Since many of the guidelines leave the program writer to decide
what is best, there is no single best or correct example of how to use Ada.
Instead, you will find several styles that differ from your own that may
deserve consideration.
There are some guidelines that rarely have exceptions and leave little room
for choice. When you find that these examples do not reflect the way you
normally code, investigate why by finding the guideline that was followed and
study the rationale. This practical exercise should be helpful in identifying
potential areas of improvement (whether for you or this style guide!).
The Menu-Driven User Interface example provides a short example demonstrating
many of the guidelines. It also provides the basic types for later examples.
The two versions of the Dining Philosophers example program demonstrate the
portability of Ada packages and tasking. They were provided by Dr. Michael B.
Feldman. Most of the available compilers can successfully compile these
examples. They have also been successfully tested on a variety of platforms.
10.1 MENU-DRIVEN USER INTERFACE
The program implements a simple menu-driven user interface that can be used as
the front end for a variety of applications. It consists of a package for
locally defined types; SPC_Numeric_Types; instantiations of Input/Output
packages for those types; a package to perform ASCII terminal I/O for
generating menus, writing prompts, and receiving user input; Terminal_IO; and
finally an example using the terminal I/O routines; Example.
Within Terminal_IO, subprogram names are overloaded when several subprograms
perform the same general function but for different data types.
The body for Terminal_IO uses separate compilation capabilities for a
subprogram, Display_Menu, that is larger and more involved than the rest.
Note, all literals that would be required are defined as constants. Nested
loops, where they exist, are also named. The numeric "get" functions defined
in the body of package, Terminal_IO, encapsulate exception handlers within a
loop. Where locally defined types could not be used, there is a comment
explaining the reason. The use of short circuit control forms, both on an if
and an exit statement, are also illustrated.
The information that would have been in the file headers is redundant since it
is contained in the title page of this book. The file headers are omitted from
the following listings:
------------------------------------------------------------------------
package SPC_Numeric_Types is
type Tiny_Integer is range -2** 7 .. 2** 7 - 1;
type Medium_Integer is range -2**15 .. 2**15 - 1;
type Big_Integer is range -2**31 .. 2**31 - 1;
subtype Tiny_Natural is
Tiny_Integer range 0 .. Tiny_Integer'Last;
subtype Medium_Natural is
Medium_Integer range 0 .. Medium_Integer'Last;
subtype Big_Natural is
Big_Integer range 0 .. Big_Integer'Last;
subtype Tiny_Positive is
Tiny_Integer range 1 .. Tiny_Integer'Last;
subtype Medium_Positive is
Medium_Integer range 1 .. Medium_Integer'Last;
subtype Big_Positive is
Big_Integer range 1 .. Big_Integer'Last;
type Medium_Float is digits 6;
type Big_Float is digits 9;
subtype Probabilities is Medium_Float range 0.0 .. 1.0;
function Min (Left : in Tiny_Integer;
Right : in Tiny_Integer)
return Tiny_Integer;
function Max (Left : in Tiny_Integer;
Right : in Tiny_Integer)
return Tiny_Integer;
-- Additional function declarations to return the minimum and maximum
--| values for each type.
end SPC_Numeric_Types;
------------------------------------------------------------------------
package body SPC_Numeric_Types is
---------------------------------------------------------------------
function Min (Left : in Tiny_Integer;
Right : in Tiny_Integer)
return Tiny_Integer is
begin
if Left < Right then
return Left;
else -- Left >= Right
return Right;
end if;
end Min;
---------------------------------------------------------------------
function Max (Left : in Tiny_Integer;
Right : in Tiny_Integer)
return Tiny_Integer is
begin
if Left > Right then
return Left;
else -- Left <= Right
return Right;
end if;
end Max;
---------------------------------------------------------------------
-- Additional functions to return minimum and maximum value for each
--| type defined in the package.
end SPC_Numeric_Types;
------------------------------------------------------------------------
with SPC_Numeric_Types;
with Text_IO;
package SPC_Small_Integer_IO is
new Text_IO.Integer_IO (SPC_Numeric_Types.Tiny_Integer);
with SPC_Numeric_Types;
with Text_IO;
package Medium_Integer_IO is
new Text_IO.Integer_IO (SPC_Numeric_Types.Medium_Integer);
with SPC_Numeric_Types;
with Text_IO;
package Big_Integer_IO is
new Text_IO.Integer_IO (SPC_Numeric_Types.Big_Integer);
with SPC_Numeric_Types;
with Text_IO;
package Medium_Float_IO is
new Text_IO.Float_IO (SPC_Numeric_Types.Medium_Float);
with SPC_Numeric_Types;
with Text_IO;
package Big_Float_IO is
new Text_IO.Float_IO (SPC_Numeric_Types.Big_Float);
------------------------------------------------------------------------
with SPC_Numeric_Types;
use SPC_Numeric_Types;
package Terminal_IO is
Max_File_Name_Length : constant := 30;
Max_Line : constant := 30;
subtype Alpha_Numeric is Character range '0' .. 'Z';
subtype Line is String (1 .. Max_Line);
Empty_Line : constant Line := (others => ' ');
type Menu is array (Alpha_Numeric) of Line;
subtype File_Name is String (1 .. Max_File_Name_Length);
procedure Get_File_Name (Prompt : in String;
Name : out File_Name;
Name_Length : out Natural);
function Yes (Prompt : in String) return Boolean;
function Get (Prompt : in String) return Medium_Integer;
function Get (Prompt : in String) return Medium_Float;
procedure Display_Menu (Title : in String;
Options : in Menu;
Choice : out Alpha_Numeric);
procedure Pause (Prompt : in String);
procedure Pause;
procedure Put (Integer_Value : in Medium_Integer);
procedure Put (Real_Value : in Medium_Float);
procedure Put (Label : in String;
Integer_Value : in Medium_Integer);
procedure Put (Label : in String;
Real_Value : in Medium_Float);
procedure Put_Line (Integer_Value : in Medium_Integer);
procedure Put_Line (Real_Value : in Medium_Float);
procedure Put_Line (Label : in String;
Integer_Value : in Medium_Integer);
procedure Put_Line (Label : in String;
Real_Value : in Medium_Float);
end Terminal_IO;
------------------------------------------------------------------------
with Medium_Integer_IO;
with Medium_Float_IO;
with Text_IO;
package body Terminal_IO is
-- simple terminal i/o routines
subtype Response is String (1 .. 20);
Prompt_Column : constant := 30;
Question_Mark : constant String := " ? ";
Standard_Prompt : constant String := " ==> ";
Blank : constant Character := ' ';
Real_Fore : constant := 4;
Real_Aft : constant := 3;
Integer_Width : constant := 4;
---------------------------------------------------------------------
procedure Put_Prompt (Prompt : in String;
Question : in Boolean := False) is
use Text_IO;
begin
Put(Prompt);
if Question then
Put(Question_Mark);
end if;
Set_Col(Prompt_Column);
Put(Standard_Prompt);
end Put_Prompt;
---------------------------------------------------------------------
function Yes (Prompt : in String) return Boolean is
Response_String : Response := (others => Blank);
Response_String_Length : Natural;
begin -- Yes
Get_Response:
loop
Put_Prompt(Prompt, Question => True);
Text_IO.Get_Line(Response_String, Response_String_Length);
Find_First_Non_Blank_Character:
for Position in 1 .. Response_String_Length loop
if Response_String(Position) /= Blank then
return Response_String(Position) = 'Y' or
Response_String(Position) = 'y';
end if;
end loop Find_First_Non_Blank_Character;
-- issue prompt until non-blank responses
Text_IO.New_Line;
end loop Get_Response;
end Yes;
---------------------------------------------------------------------
procedure Get_File_Name (Prompt : in String;
Name : out File_Name;
Name_Length : out Natural) is
begin
Put_Prompt(Prompt);
Text_IO.Get_Line(Name, Name_Length);
end Get_File_Name;
---------------------------------------------------------------------
function Get (Prompt : in String) return Medium_Integer is
Response_String : Response := (others => Blank);
Last : Natural; -- Required by Get_Line.
Value : Medium_Integer;
begin -- Get
loop
Catch_Input_Errors:
begin
Put_Prompt(Prompt);
Text_IO.Get_Line(Response_String, Last);
Value :=
Medium_Integer'Value(Response_String(1 .. Last));
return Value;
exception
when others =>
Text_IO.Put_Line("Please enter an integer");
end Catch_Input_Errors;
end loop;
end Get;
---------------------------------------------------------------------
procedure Display_Menu (Title : in String;
Options : in Menu;
Choice : out Alpha_Numeric) is separate;
---------------------------------------------------------------------
procedure Pause (Prompt : in String) is
begin
Text_IO.Put_Line(Prompt);
Pause;
end Pause;
---------------------------------------------------------------------
procedure Pause is
Buffer : Response;
Last : Natural;
begin -- Pause
Text_IO.Put("Press return to continue");
Text_IO.Get_Line(Buffer, Last);
end Pause;
---------------------------------------------------------------------
function Get (Prompt : in String) return Medium_Float is
Value : Medium_Float;
begin -- Get
loop
Catch_Input_Errors:
begin
Put_Prompt(Prompt);
Medium_Float_IO.Get(Value);
Text_IO.Skip_Line;
return Value;
exception
when others =>
Text_IO.Skip_Line;
Text_IO.Put_Line("Please enter a real number");
end Catch_Input_Errors;
end loop;
end Get;
---------------------------------------------------------------------
procedure Put (Integer_Value : in Medium_Integer) is
begin
Medium_Integer_IO.Put(Integer_Value, Width => Integer_Width);
end Put;
---------------------------------------------------------------------
procedure Put (Real_Value : in Medium_Float) is
begin
Medium_Float_IO.Put
(Real_Value,
Fore => Real_Fore,
Aft => Real_Aft,
Exp => 0);
end Put;
---------------------------------------------------------------------
procedure Put (Label : in String;
Integer_Value : in Medium_Integer) is
begin
Text_IO.Put(Label);
Medium_Integer_IO.Put(Integer_Value);
end Put;
---------------------------------------------------------------------
procedure Put (Label : in String;
Real_Value : in Medium_Float) is
begin
Text_IO.Put(Label);
Medium_Float_IO.Put
(Real_Value,
Fore => Real_Fore,
Aft => Real_Aft,
Exp => 0);
end Put;
---------------------------------------------------------------------
procedure Put_Line (Integer_Value : in Medium_Integer) is
begin
Terminal_IO.Put(Integer_Value);
Text_IO.New_Line;
end Put_Line;
---------------------------------------------------------------------
procedure Put_Line (Real_Value : in Medium_Float) is
begin
Terminal_IO.Put(Real_Value);
Text_IO.New_Line;
end Put_Line;
---------------------------------------------------------------------
procedure Put_Line (Label : in String;
Integer_Value : in Medium_Integer) is
begin
Terminal_IO.Put(Label, Integer_Value);
Text_IO.New_Line;
end Put_Line;
---------------------------------------------------------------------
procedure Put_Line (Label : in String;
Real_Value : in Medium_Float) is
begin
Terminal_IO.Put(Label, Real_Value);
Text_IO.New_Line;
end Put_Line;
---------------------------------------------------------------------
end Terminal_IO;
------------------------------------------------------------------------
separate (Terminal_IO)
procedure Display_Menu (Title : in String;
Options : in Menu;
Choice : out Alpha_Numeric) is
Left_Column : constant := 15;
Right_Column : constant := 20;
type Alpha_Array is array (Alpha_Numeric) of Boolean;
Valid : Boolean;
Valid_Option : Alpha_Array := (others => False);
---------------------------------------------------------------------
procedure Draw_Menu (Title : in String;
Options : in Menu) is
use Text_IO;
begin
New_Page;
New_Line;
Set_Col(Right_Column);
Put_Line(Title);
New_Line;
for Choice in Alpha_Numeric loop
if Options(Choice) /= Empty_Line then
Valid_Option(Choice) := True;
Set_Col(Left_Column);
Put(Choice & " -- ");
Put_Line(Options(Choice));
end if;
end loop;
end Draw_Menu;
---------------------------------------------------------------------
procedure Get_Response (Valid : out Boolean;
Choice : out Alpha_Numeric) is
Buffer_Size : constant := 20;
Dummy : constant Alpha_Numeric := 'X';
First_Char : Character;
Buffer : String (1 .. Buffer_Size);
-- IMPLEMENTATION NOTE:
-- The following two declarations do not use locally defined types
--| because a variable of type Natural is required by the
--| Text_IO routines for strings, and there is no relational
--| operator defined for our local Tiny_, Medium_, or
--| Big_Positive and the standard type Natural.
Last : Natural;
Index : Positive;
------------------------------------------------------------------
function Upper_Case (Current_Char : in Character)
return Character is
Case_Difference : constant
:= Character'Pos('a') - Character'Pos('A');
begin -- Upper_Case
if Current_Char in 'a' .. 'z' then
return
Character'Val
(Character'Pos(Current_Char) - Case_Difference);
else -- Current_Char not in 'a' .. 'z'
return Current_Char;
end if;
end Upper_Case;
------------------------------------------------------------------
use Text_IO;
begin -- Get_Response
New_Line;
Set_Col(Left_Column);
Put(Standard_Prompt);
Get_Line(Buffer, Last);
Index := Buffer'First;
for Position in Buffer'First .. Last loop
Index := Position;
exit when Upper_Case(Buffer(Index)) in Alpha_Numeric;
end loop;
First_Char := Upper_Case(Buffer(Index));
if First_Char in Alpha_Numeric and then
Valid_Option(First_Char) then
Valid := True;
Choice := First_Char;
else -- not a valid character
Valid := False;
Choice := Dummy;
end if;
end Get_Response;
---------------------------------------------------------------------
procedure Beep is
begin
Text_IO.Put(ASCII.Bel);
end Beep;
---------------------------------------------------------------------
begin -- Display_Menu
loop
Draw_Menu(Title, Options);
Get_Response(Valid, Choice);
exit when Valid;
Beep;
end loop;
end Display_Menu;
------------------------------------------------------------------------
with SPC_Numeric_Types;
with Terminal_IO;
procedure Example is
package TIO renames Terminal_IO;
Example_Menu : constant TIO.Menu := TIO.Menu'
('A' => "Add item ",
'D' => "Delete item ",
'M' => "Modify item ",
'Q' => "Quit ",
others => TIO.Empty_Line);
User_Choice : TIO.Alpha_Numeric;
Item : SPC_Numeric_Types.Medium_Integer;
begin -- Example
loop
TIO.Display_Menu("Example Menu", Example_Menu, User_Choice);
case User_Choice is
when 'A' => Item := TIO.Get ("Item to add");
when 'D' => Item := TIO.Get ("Item to delete");
when 'M' => Item := TIO.Get ("Item to modify");
when 'Q' => exit;
when others => -- error has already been signaled to user
null;
end case;
end loop;
end Example;
------------------------------------------------------------------------
-- This is what is displayed, anything but A, D, M or Q beeps
--
-- Example Menu
--
-- A -- Add item
-- D -- Delete item
-- M -- Modify item
-- Q -- Quit
--
-- ==>
10.2 LINE-ORIENTED PORTABLE DINING PHILOSOPHERS EXAMPLE
Michael B. Feldman
Dept. of Electrical Engineering and Computer Science
The George Washington University
Washington, DC 20052
(202) 994-5253
mfeldman@seas.gwu.edu
Copyright 1991, Michael B. Feldman
These programs may be freely copied, distributed, and modified for educational
purposes but not for profit. If you modify or enhance the program (for
example, to use other display systems), please send Dr. Feldman a copy of the
modified code, either on diskette or by e-mail.
This system is an elaborate implementation of Edsger Dijkstra's famous Dining
Philosophers; a classical demonstration of deadlock problems in concurrent
programming.
This example uses the numeric types from the Menu-Driven User Interface
example. At least one compiler (Ada/Ed) does not support floating-point
precision greater than 6 digits. In this case, Big_Float will need a smaller
precision (digits 6 for Ada/Ed).
------------------------------------------------------------------------
with SPC_Numeric_Types;
use SPC_Numeric_Types;
package Random is
-- Simple pseudo-random number generator package.
-- Adapted from the Ada literature by
-- Michael B. Feldman, The George Washington University,
-- November 1990.
procedure Set_Seed (N : in Medium_Positive);
function Unit_Random return Medium_Float;
--returns a float >=0.0 and <1.0
function Random_Int (N : in Medium_Positive)
return Medium_Positive;
--return a random integer in the range 1..N
end Random;
------------------------------------------------------------------------
package Chop is
task type Stick is
entry Pick_Up;
entry Put_Down;
end Stick;
end Chop;
------------------------------------------------------------------------
with SPC_Numeric_Types;
use SPC_Numeric_Types;
package Phil is
task type Philosopher is
entry Come_To_Life (My_ID : in Medium_Natural;
Chopstick1 : in Medium_Natural;
Chopstick2 : in Medium_Natural);
end Philosopher;
type States is
(Breathing, Thinking, Eating, Done_Eating,
Got_One_Stick, Got_Other_Stick);
end Phil;
------------------------------------------------------------------------
with SPC_Numeric_Types;
use SPC_Numeric_Types;
with Chop;
with Phil;
package Room is
Table_Size : constant := 5;
subtype Table_Type is Medium_Natural range 1 .. Table_Size;
Sticks : array (Table_Type) of Chop.Stick;
task Head_Waiter is
entry Open_The_Room;
entry Report_State (Which_Phil : in Table_Type;
State : in Phil.States;
How_Long : in Medium_Natural := 0);
end Head_Waiter;
end Room;
------------------------------------------------------------------------
with Room;
procedure Diners is
begin
Room.Head_Waiter.Open_The_Room;
loop
delay 20.0;
end loop;
end Diners;
------------------------------------------------------------------------
with Calendar;
with SPC_Numeric_Types;
use Calendar;
use SPC_Numeric_Types;
package body Random is
-- Body of random number generator package.
-- Adapted from the Ada literature by
-- Michael B. Feldman, The George Washington University,
-- November 1990.
Modulus : constant := 9_317;
subtype Seed_Range is Medium_Integer range 0 .. Modulus - 1;
Seed : Seed_Range;
Default_Seed : Seed_Range;
procedure Set_Seed
(N : in Medium_Positive) is separate;
function Unit_Random
return Medium_Float is separate;
function Random_Int
(N : in Medium_Positive)
return Medium_Positive is separate;
begin
Default_Seed :=
Medium_Integer(Big_Integer(Seconds(Clock)) mod Modulus);
Seed := Default_Seed;
end Random;
------------------------------------------------------------------------
separate (Random)
procedure Set_Seed (N : in Medium_Positive) is
begin
Seed := Seed_Range(N);
end Set_Seed;
------------------------------------------------------------------------
separate (Random)
function Unit_Random return Medium_Float is
Multiplier : constant := 421;
Increment : constant := 2_073;
Result : Medium_Float;
begin -- Unit_Random
Seed := (Multiplier * Seed + Increment) mod Modulus;
Result := Medium_Float(Seed) / Medium_Float(Modulus);
return Result;
exception
when Constraint_Error | Numeric_Error =>
Seed := Medium_Integer
((Multiplier * Big_Integer(Seed) + Increment) mod
Modulus);
Result := Medium_Float(Seed) / Medium_Float(Modulus);
return Result;
end Unit_Random;
------------------------------------------------------------------------
separate (Random)
function Random_Int (N : in Medium_Positive)
return Medium_Positive is
Result : Medium_Positive range 1 .. N;
begin -- Random_Int
Result := Medium_Positive(Medium_Float(N) * Unit_Random + 0.5);
return Result;
exception
when Constraint_Error | Numeric_Error =>
return 1;
end Random_Int;
------------------------------------------------------------------------
package body Chop is
task body Stick is
begin
loop
select
accept Pick_Up;
accept Put_Down;
or
terminate;
end select;
end loop;
-- No exception handler is needed here.
end Stick;
end Chop;
------------------------------------------------------------------------
with SPC_Numeric_Types;
with Room;
with Random;
use SPC_Numeric_Types;
package body Phil is
task body Philosopher is
type Life_Time is range 1 .. 100_000;
Who_Am_I : Medium_Natural;
First_Grab : Medium_Natural;
Second_Grab : Medium_Natural;
Meal_Time : Medium_Natural;
Think_Time : Medium_Natural;
begin -- Philosopher
accept Come_To_Life (My_ID : in Medium_Natural;
Chopstick1 : in Medium_Natural;
Chopstick2 : in Medium_Natural) do
Who_Am_I := My_ID;
First_Grab := Chopstick1;
Second_Grab := Chopstick2;
end Come_To_Life;
Room.Head_Waiter.Report_State(Who_Am_I, Breathing);
for Meal in Life_Time loop
Room.Sticks(First_Grab).Pick_Up;
Room.Head_Waiter.Report_State
(Who_Am_I, Got_One_Stick, First_Grab);
Room.Sticks(Second_Grab).Pick_Up;
Room.Head_Waiter.Report_State
(Who_Am_I, Got_Other_Stick, Second_Grab);
Meal_Time := Random.Random_Int(10);
Room.Head_Waiter.Report_State(Who_Am_I, Eating, Meal_Time);
delay Duration(Meal_Time);
Room.Head_Waiter.Report_State(Who_Am_I, Done_Eating);
Room.Sticks(First_Grab).Put_Down;
Room.Sticks(Second_Grab).Put_Down;
Think_Time := Random.Random_Int(10);
Room.Head_Waiter.Report_State
(Who_Am_I, Thinking, Think_Time);
delay Duration(Think_Time);
end loop;
-- No exception handler is needed here.
end Philosopher;
end Phil;
------------------------------------------------------------------------
with SPC_Numeric_Types;
use SPC_Numeric_Types;
with Text_IO;
with Chop;
with Phil;
with Calendar;
pragma Elaborate (Phil);
package body Room is
-- A line-oriented version of the Room package, for line-oriented
-- terminals like IBM 3270's where the user cannot do ASCII
-- screen control.
-- This is the only file in the dining philosophers system that
-- needs changing to use in a line-oriented environment.
-- Michael B. Feldman, The George Washington University,
-- November 1990.
Phils : array (Table_Type) of Phil.Philosopher;
type Phil_Name is (Dijkstra, Texel, Booch, Ichbiah, Stroustrup);
task body Head_Waiter is
T : Medium_Natural;
Start_Time : Calendar.Time;
Phil_Names : constant array (Table_Type) of String (1 .. 18)
:= ("Eddy Dijkstra ", "Putnam Texel ",
"Grady Booch ", "Jean Ichbiah ",
"Bjarne Stroustrup ");
Blanks : constant String := " ";
begin -- Head_Waiter
accept Open_The_Room;
Start_Time := Calendar.Clock;
Phils(1).Come_To_Life(1, 1, 2);
Phils(3).Come_To_Life(3, 3, 4);
Phils(2).Come_To_Life(2, 2, 3);
Phils(5).Come_To_Life(5, 1, 5);
Phils(4).Come_To_Life(4, 4, 5);
loop
select
accept Report_State (Which_Phil : in Table_Type;
State : in Phil.States;
How_Long : in Medium_Natural
:= 0) do
T := Medium_Natural
(Calendar."-"(Calendar.Clock, Start_Time));
Text_IO.Put ("T=" & Medium_Natural'Image(T) & " " &
Blanks(1 .. Positive(Which_Phil)) &
Phil_Names(Which_Phil));
case State is
when Phil.Breathing =>
Text_IO.Put("Breathing");
when Phil.Thinking =>
Text_IO.Put ("Thinking" &
Medium_Natural'Image(How_Long) &
" seconds.");
when Phil.Eating =>
Text_IO.Put ("Eating" &
Medium_Natural'Image(How_Long) &
" seconds.");
when Phil.Done_Eating =>
Text_IO.Put("Yum-yum (burp)");
when Phil.Got_One_Stick =>
Text_IO.Put ("First chopstick" &
Medium_Natural'Image(How_Long));
when Phil.Got_Other_Stick =>
Text_IO.Put ("Second chopstick" &
Medium_Natural'Image(How_Long));
end case; -- State
Text_IO.New_Line;
end Report_State;
or
terminate;
end select;
end loop;
-- An exception handler is not needed here.
end Head_Waiter;
end Room;
------------------------------------------------------------------------
10.3 WINDOW-ORIENTED PORTABLE DINING PHILOSOPHERS EXAMPLE
Michael B. Feldman
Dept. of Electrical Engineering and Computer Science
The George Washington University
Washington, DC 20052
(202) 994-5253
mfeldman@seas.gwu.edu
Copyright 1991, Michael B. Feldman
These programs may be freely copied, distributed, and modified for educational
purposes but not for profit. If you modify or enhance the program (for
example, to use other display systems), please send Dr. Feldman a copy of the
modified code, either on diskette or by e-mail.
This system is an elaborate implementation of Edsger Dijkstra's famous Dining
Philosophers; a classical demonstration of deadlock problems in concurrent
programming.
This example builds on some of the utilities found in the Line-Oriented
example.
------------------------------------------------------------------------
package Screen is
-- Procedures for drawing pictures on ANSI Terminal Screen
Screen_Depth : constant := 24;
Screen_Width : constant := 80;
subtype Depth is Integer range 1 .. Screen_Depth;
subtype Width is Integer range 1 .. Screen_Width;
procedure Beep;
procedure Clear_Screen;
procedure Move_Cursor (Column : in Width;
Row : in Depth);
end Screen;
------------------------------------------------------------------------
with Screen;
use Screen;
package Windows is
type Window is private;
procedure Open
(W : in out Window; -- Window variable returned
Row : in Depth; -- Upper left corner
Column : in Width;
Height : in Depth; -- Size of window
Width : in Screen.Width);
-- Create a window variable and open the window for writing.
-- No checks for overlap of windows are made.
procedure Close (W : in out Window);
-- Close window and clear window variable.
procedure Title (W : in out Window;
Name : in String;
Under : in Character);
-- Put a title name at the top of the window. If the parameter
-- Under is nonblank, underline the title with the
-- specified character.
procedure Borders (W : in out Window;
Corner : in Character;
Down : in Character;
Across : in Character);
-- Draw border around current writable area in window with
-- characters specified. Call this BEFORE Title.
procedure Go_To_Row_Column (W : in out Window;
Row : in Depth;
Column : in Width);
-- Goto the row and column specified. Coordinates are relative
-- to the upper left corner of window, which is (1, 1)
procedure Put
(W : in out Window;
Ch : in Character);
-- put one character to the window.
-- If end of column, go to the next row.
-- If end of window, go to the top of the window.
procedure Put_String (W : in out Window;
S : in String);
-- put a string to window.
procedure New_Line (W : in out Window);
-- Go to beginning of next line. Next line is
-- not blanked until next character is written
private
type Window is
record
Current_Row : Depth; -- Current cursor row
First_Row : Depth;
Last_Row : Depth;
Current_Column : Width; -- Current cursor column
First_Column : Width;
Last_Column : Width;
end record;
end Windows;
------------------------------------------------------------------------
with Text_IO;
package body Screen is
package My_Int_IO is new Text_IO.Integer_IO (Integer);
-- Procedures for drawing pictures on ANSI Terminal Screen
---------------------------------------------------------------------
procedure Beep is
begin
Text_IO.Put(Item => ASCII.Bel);
end Beep;
---------------------------------------------------------------------
procedure Clear_Screen is
begin
Text_IO.Put(Item => ASCII.Esc);
Text_IO.Put(Item => "[2J");
end Clear_Screen;
---------------------------------------------------------------------
procedure Move_Cursor (Column : in Width;
Row : in Depth) is
begin
Text_IO.New_Line;
Text_IO.Put(Item => ASCII.Esc);
Text_IO.Put("[");
My_Int_IO.Put (Item => Row,
Width => 1);
Text_IO.Put(Item => ';');
My_Int_IO.Put (Item => Column,
Width => 1);
Text_IO.Put(Item => 'f');
end Move_Cursor;
---------------------------------------------------------------------
end Screen;
------------------------------------------------------------------------
with Text_IO;
with Medium_Integer_IO;
with Screen;
use Text_IO;
use Medium_Integer_IO;
use Screen;
package body Windows is
Cursor_Row : Depth := 1; -- Current cursor position
Cursor_Col : Width := 1;
---------------------------------------------------------------------
procedure Open
(W : in out Window;
Row : in Depth;
Column : in Width;
Height : in Depth;
Width : in Screen.Width) is
--Put the Window's cursor in upper left corner
begin
W.Current_Row := Row;
W.First_Row := Row;
W.Last_Row := Row + Height - 1;
W.Current_Column := Column;
W.First_Column := Column;
W.Last_Column := Column + Width - 1;
end Open;
---------------------------------------------------------------------
procedure Close (W : in out Window) is
begin
null;
end Close;
---------------------------------------------------------------------
procedure Title (W : in out Window;
Name : in String;
Under : in Character) is
-- Put name at the top of the Window. If Under nonblank,
-- underline the title.
begin
-- Put name on top line
W.Current_Column := W.First_Column;
W.Current_Row := W.First_Row;
Put_String(W, Name);
New_Line(W);
-- Underline name if desired, and move the First line
-- of the Window below the title
if Under = ' ' then
W.First_Row := W.First_Row + 1;
else -- put nonblank characters under title
for I in W.First_Column .. W.Last_Column loop
Put(W, Under);
end loop;
New_Line(W);
W.First_Row := W.First_Row + 2;
end if;
end Title;
---------------------------------------------------------------------
procedure Go_To_Row_Column (W : in out Window;
Row : in Depth;
Column : in Width) is
-- Relative to writable Window boundaries, of course
begin
W.Current_Row := W.First_Row + Row;
W.Current_Column := W.First_Column + Column;
end Go_To_Row_Column;
---------------------------------------------------------------------
procedure Borders (W : in out Window;
Corner : in Character;
Down : in Character;
Across : in Character) is
-- Draw border around current writable area in Window
-- with characters. Call this BEFORE Title.
begin
-- Put top line of border
Screen.Move_Cursor(W.First_Column, W.First_Row);
Text_IO.Put(Corner);
for J in W.First_Column + 1 .. W.Last_Column - 1 loop
Text_IO.Put(Across);
end loop;
Text_IO.Put(Corner);
-- Put the two side lines
for I in W.First_Row + 1 .. W.Last_Row - 1 loop
Screen.Move_Cursor(W.First_Column, I);
Text_IO.Put(Down);
Screen.Move_Cursor(W.Last_Column, I);
Text_IO.Put(Down);
end loop;
-- Put the bottom line of the border
Screen.Move_Cursor(W.First_Column, W.Last_Row);
Text_IO.Put(Corner);
for J in W.First_Column + 1 .. W.Last_Column - 1 loop
Text_IO.Put(Across);
end loop;
Text_IO.Put(Corner);
-- Put the cursor at the very end of the Window
Cursor_Row := W.Last_Row;
Cursor_Col := W.Last_Column + 1;
-- Make the Window smaller by one character on each side
W.First_Row := W.First_Row + 1;
W.Current_Row := W.First_Row;
W.Last_Row := W.Last_Row - 1;
W.First_Column := W.First_Column + 1;
W.Current_Column := W.First_Column;
W.Last_Column := W.Last_Column - 1;
end Borders;
---------------------------------------------------------------------
procedure Erase_To_End_Of_Line (W : in out Window) is
begin
Screen.Move_Cursor(W.Current_Column, W.Current_Row);
for I in W.Current_Column .. W.Last_Column loop
Text_IO.Put(' ');
end loop;
Screen.Move_Cursor(W.Current_Column, W.Current_Row);
Cursor_Col := W.Current_Column;
Cursor_Row := W.Current_Row;
end Erase_To_End_Of_Line;
---------------------------------------------------------------------
procedure Put (W : in out Window;
Ch : in Character) is
-- If after end of line, move to First character of next line
-- If about to write First character on line, blank rest of
-- line.
-- Put character.
begin
if Ch = ASCII.CR then
New_Line(W);
return;
end if;
-- If at end of current line, move to next line
if W.Current_Column > W.Last_Column then
if W.Current_Row = W.Last_Row then
W.Current_Row := W.First_Row;
else -- not at end of current line
W.Current_Row := W.Current_Row + 1;
end if;
W.Current_Column := W.First_Column;
end if;
-- If at W.First char, erase line
if W.Current_Column = W.First_Column then
Erase_To_End_Of_Line(W);
end if;
-- Put physical cursor at Window's cursor
if Cursor_Col /= W.Current_Column or
Cursor_Row /= W.Current_Row then
Screen.Move_Cursor(W.Current_Column, W.Current_Row);
Cursor_Row := W.Current_Row;
end if;
if Ch = ASCII.BS then
-- Special backspace handling
if W.Current_Column /= W.First_Column then
Text_IO.Put(Ch);
W.Current_Column := W.Current_Column - 1;
end if;
else -- character is not a backspace, so just write it
Text_IO.Put(Ch);
W.Current_Column := W.Current_Column + 1;
end if;
Cursor_Col := W.Current_Column;
end Put;
---------------------------------------------------------------------
procedure New_Line (W : in out Window) is
Col : Width;
-- If not after line, blank rest of line.
-- Move to First character of next line
begin -- New_Line
if W.Current_Column = 0 then
Erase_To_End_Of_Line(W);
end if;
if W.Current_Row = W.Last_Row then
W.Current_Row := W.First_Row;
else -- not at bottom of screen
W.Current_Row := W.Current_Row + 1;
end if;
W.Current_Column := W.First_Column;
end New_Line;
---------------------------------------------------------------------
procedure Put_String (W : in out Window;
S : in String) is
begin
for I in S'First .. S'Last loop
Put(W, S(I));
end loop;
end Put_String;
---------------------------------------------------------------------
begin -- Windows
Screen.Clear_Screen;
Screen.Move_Cursor(1, 1);
end Windows;
------------------------------------------------------------------------
with SPC_Numeric_Types;
with Windows;
with Chop;
with Phil;
with Calendar;
use SPC_Numeric_Types;
pragma Elaborate (Phil);
package body Room is
Phils : array (Table_Type) of Phil.Philosopher;
Phil_Windows : array (Table_Type) of Windows.Window;
type Phil_Names is (Dijkstra, Texel, Booch, Ichbiah, Stroustrup);
task body Head_Waiter is
T : Medium_Positive;
Start_Time : Calendar.Time;
begin -- Head_Waiter
accept Open_The_Room;
Start_Time := Calendar.Clock;
Windows.Open (W => Phil_Windows(1),
Row => 1,
Column => 23,
Height => 7,
Width => 30);
Windows.Borders(Phil_Windows(1), '+', '|', '-');
Windows.Title (Phil_Windows(1), "Eddy Dijkstra", '-');
Phils(1).Come_To_Life(1, 1, 2);
Windows.Open (W => Phil_Windows(3),
Row => 9,
Column => 50,
Height => 7,
Width => 30);
Windows.Borders(Phil_Windows(3), '+', '|', '-');
Windows.Title (Phil_Windows(3), "Grady Booch", '-');
Phils(3).Come_To_Life(3, 3, 4);
Windows.Open (W => Phil_Windows(2),
Row => 9,
Column => 2,
Height => 7,
Width => 30);
Windows.Borders(Phil_Windows(2), '+', '|', '-');
Windows.Title (Phil_Windows(2), "Putnam Texel", '-');
Phils(2).Come_To_Life(2, 2, 3);
Windows.Open (W => Phil_Windows(5),
Row => 17,
Column => 41,
Height => 7,
Width => 30);
Windows.Borders(Phil_Windows(5), '+', '|', '-');
Windows.Title (Phil_Windows(5), "Bjarne Stroustrup", '-');
Phils(5).Come_To_Life(5, 1, 5);
Windows.Open (W => Phil_Windows(4),
Row => 17,
Column => 8,
Height => 7,
Width => 30);
Windows.Borders(Phil_Windows(4), '+', '|', '-');
Windows.Title (Phil_Windows(4), "Jean Ichbiah", '-');
Phils(4).Come_To_Life(4, 4, 5);
loop
select
accept Report_State
(Which_Phil : in Table_Type;
State : in Phil.States;
How_Long : in Medium_Natural := 0) do
T :=
Medium_Natural
(Calendar."-"(Calendar.Clock, Start_Time));
Windows.Put_String
(Phil_Windows(Which_Phil),
"T=" & Medium_Natural'Image(T) & " ");
case State is
when Phil.Breathing =>
Windows.Put_String
(Phil_Windows(Which_Phil),
"Breathing...");
Windows.New_Line(Phil_Windows(Which_Phil));
when Phil.Thinking =>
Windows.Put_String
(Phil_Windows(Which_Phil),
"Thinking" & Medium_Natural'Image(How_Long) &
" seconds.");
Windows.New_Line(Phil_Windows(Which_Phil));
when Phil.Eating =>
Windows.Put_String
(Phil_Windows(Which_Phil),
"Eating" & Medium_Natural'Image(How_Long) &
" seconds.");
Windows.New_Line(Phil_Windows(Which_Phil));
when Phil.Done_Eating =>
Windows.Put_String
(Phil_Windows(Which_Phil),
"Yum-yum (burp)");
Windows.New_Line(Phil_Windows(Which_Phil));
when Phil.Got_One_Stick =>
Windows.Put_String
(Phil_Windows(Which_Phil),
"First chopstick" &
Medium_Natural'Image(How_Long));
Windows.New_Line(Phil_Windows(Which_Phil));
when Phil.Got_Other_Stick =>
Windows.Put_String
(Phil_Windows(Which_Phil),
"Second chopstick" &
Medium_Natural'Image(How_Long));
Windows.New_Line(Phil_Windows(Which_Phil));
end case;
end Report_State;
or
terminate;
end select;
end loop;
-- An exception handler is not needed here.
end Head_Waiter;
end Room;
------------------------------------------------------------------------
APPENDIX A
Map from Ada Language Reference Manual to Guidelines
1. Introduction
1.1 Scope of the Standard
1.1.1 Extent of the Standard
1.1.2 Conformity of an Implementation with the Standard
1.2 Structure of the Standard
1.3 Design Goals and Sources
1.4 Language Summary
1.5 Method of Description and Syntax Notation 2.1.8
1.6 Classification of Errors 5.9
2. Lexical Elements
2.1 Character Set
2.2 Lexical Elements, Separators, and Delimiters 2.1.1
2.3 Identifiers 3.1.1, 3.1.3, 3.1.4, 3.2
5.2.1, 5.5.4, 8.1.1, 8.1.2
2.4 Numeric Literals 3.1.2, 3.2.5, 7.2.6
2.4.1 Decimal Literals
2.4.2 Based Literals
2.5 Character Literals 3.2.5
2.6 String Literals 2.1.1, 2.1.2, 2.1.4, 3.2.5
2.7 Comments 2.1.4, 2.1.7, 3.3, 5.2.1, 5.6.7
5.6.8, 7.1.3, 7.1.5, 7.2.5, 8.3.5
2.8 Pragmas 8.4.2, 9.1.1
2.9 Reserved Words 3.1.3
2.10 Allowable Replacements of Characters
3. Declarations and Types 2.1.4, 2.1.6, 2.1.8, 4.1.4, 5.3
3.1 Declarations 3.2.1
3.2 Objects and Named Numbers 3.2.3, 3.2.5, 4.1.5, 7.2.6, 8.2.1
3.2.1 Object Declarations 4.1.6, 5.9.6
3.2.2 Number Declarations
3.3 Types and Subtypes 3.2.2, 3.4.1, 4.1.5, 7.2.7, 8.2.3, 9.1.5
3.3.1 Type Declarations 3.3.5
3.3.2 Subtype Declarations 5.3.1, 5.5.1, 5.6.3, 5.7.2, 7.2.7, 9.1.5
3.3.3 Classification of Operations
3.4 Derived Types 3.4.1, 5.3.1, 7.2.7, 9.1.5
3.5 Scalar Types 3.4.1, 5.3.1, 5.5.1
3.5.1 Enumeration Types 2.1.4, 3.4.2, 5.6.3, 5.7.1
3.5.2 Character Types
3.5.3 Boolean Types 3.2.3
3.5.4 Integer Types 7.1.1, 7.1.2, 7.2.1, 7.2.5
3.5.5 Operations of Discrete Types
3.5.6 Real Types 7.1.1, 7.1.2, 5.5.6, 7.2.5
3.5.7 Floating Point Types 7.2.1, 7.2.2, 7.2.3
3.5.8 Operations of Floating Point Types
3.5.9 Fixed Point Types 7.1.1, 7.2.1
3.5.10 Operations of Fixed Point Types
3.6 Array Types 5.3.2, 5.5.1, 5.5.2, 5.9.3
8.2.2, 8.3.4, 9.1.3
3.6.1 Index Constraints and Discrete Ranges 5.5.2, 9.1.3
3.6.2 Operations of Array Types 5.6.2
3.6.3 The Type String 2.1.1
3.7 Record Types 2.1.2, 5.4.1, 5.4.2, 5.4.3, 5.9.3
3.7.1 Discriminants
3.7.2 Discriminant Constraints
3.7.3 Variant Parts
3.7.4 Operations of Record Types
3.8 Access Types 5.4.3, 5.9.2, 6.1.3, 7.6.6, 7.7.3
3.8.1 Incomplete Type Declarations 5.4.3
3.8.2 Operations of Access Types
3.9 Declarative Parts 2.1.4, 5.9.6
4. Names and Expressions
4.1 Names 3.2, 8.1.1, 8.1.2
4.1.1 Indexed Components
4.1.2 Slices 5.6.2
4.1.3 Selected Components
4.1.4 Attributes 8.2.4
4.2 Literals 8.2.4
4.3 Aggregates
4.3.1 Record Aggregates 5.6.10
4.3.2 Array Aggregates
4.4 Expressions 2.1.1, 2.1.3, 2.1.5, 5.5, 5.5.3, 7.2.7
4.5 Operators and Expression Evaluation 2.1.1, 2.1.3,
5.5.3, 5.7.1, 5.7.2, 5.7.4, 7.2.7
4.5.1 Logical Operators and Short-circuit Control Forms 5.5.4, 5.5.5
4.5.2 Relational Operators and Membership Tests 2.1.1, 5.5.6, 5.7.5,
7.2.8
4.5.3 Binary Adding Operators 2.1.1, 5.7.4
4.5.4 Unary Adding Operators
4.5.5. Multiplying Operators 9.1.5
4.5.6 Highest Precedence Operators 5.5.3
4.5.7 Accuracy of Operations with Real Operands 5.5.6,
7.2.2, 7.2.3, 7.2.4, 7.2.5, 7.2.7
4.6 Type Conversions 5.9.1
4.7 Qualified Expressions
4.8 Allocators 5.4.3, 6.1.3, 7.6.6
4.9 Static Expressions and Static Subtypes 3.2.5, 7.2.6, 8.2.1
4.10 Universal Expressions 7.2.6
5. Statements 2.1.2, 2.1.6, 2.1.8, 3.3.6, 5.6
5.1 Simple and Compound Statements - Sequences of Statements 5.6.1
5.2 Assignment Statement 2.1.3, 5.6.2
5.2.1 Array Assignments
5.3 If Statements 2.1.5, 3.3.7, 5.6.1, 5.6.5
5.4 Case Statements 3.3.7, 5.6.1, 5.6.3
5.5 Loop Statements 5.1.1, 5.5.1, 5.6.1, 5.6.2, 5.6.4, 5.6.6, 7.4.2
5.6 Block Statements 3.3.7, 5.1.2, 5.6.1, 5.6.9, 5.8.4, 9.1.2
5.7 Exit Statements 5.1.3, 5.6.4, 5.6.4, 5.6.5
5.8 Return Statements 5.6.8
5.9 Goto Statements 5.6.7, 5.8.1
6. Subprograms 4.1.2
6.1 Subprogram Declarations 2.1.2, 3.2.4, 3.3.3,
4.1.1, 4.1.4, 4.1.5, 4.2.1, 4.3.1,
5.2.1, 5.6.6, 7.1.3, 7.1.4
6.2 Formal Parameter Modes 2.1.5, 5.2.4, 5.9.3
6.3 Subprogram Bodies 2.1.7, 3.3.4, 3.3.7, 5.1.4, 9.1.1
6.3.1 Conformance Rules
6.3.2 Inline Expansion of Subprograms 9.1.1
6.4 Subprogram Calls 2.1.1, 4.1.3, 5.2.2, 5.6.6, 5.9.3
6.4.1 Parameter Associations 5.2.2, 5.9.8
6.4.2 Default Parameters 5.2.2, 5.2.3
6.5 Function Subprograms 2.1.1, 3.2.4, 4.1.3, 4.3.1, 5.9.6
6.6 Parameter and Result Type Profile -
Overloading of Subprograms 4.2.1
5.7.3, 8.2.5
6.7 Overloading of Operators 5.7.5
7. Packages 2.1.2, 4.1.4, 4.1.5
7.1 Package Structure 4.2.1, 4.3, 7.1.5
7.2 Package Specifications and Declarations 2.1.7, 3.2.4, 3.3.3,
4.1.1, 4.1.6, 4.2.1, 4.2.2, 4.2.4, 4.3.1,
5.1.4, 7.1.3, 7.1.5, 8.2.6, 8.3.1
7.3 Package Bodies 2.1.7, 3.3.4
4.1.1, 4.2.1, 4.3.1, 5.1.4, 5.9.1
7.4 Private Type and Deferred Constant Declarations 5.3.3
7.4.1 Private Types 5.3.3
7.4.2 Operations of a Private Type
7.4.3 Deferred Constants
7.4.4 Limited Types 5.3.3, 5.7.5, 8.3.4
7.5 Example of a Table Management Package
7.6 Example of a Text Handling Package
8. Visibility Rules 4.2, 5.7
8.1 Declarative Region 4.2.3
8.2 Scope of Declarations 4.1.6, 4.2.3, 7.6.6
8.3 Visibility 4.1.3, 4.1.4, 4.2.1, 4.2.3, 5.7.1, 5.7.3
8.4 Use Clauses 5.6.9, 5.7.1, 5.7.2
8.5 Renaming Declarations 5.6.9, 5.7.1, 5.7.2
8.6 The Package Standard 3.2.2, 7.2.1
8.7 The Context of Overload Resolution 8.2.5
9. Tasks 4.1.7, 4.2.4, 6, 7.4
9.1 Task Specifications and Task Bodies 2.1.7, 3.2.4, 3.3.7, 5.1.4
6.1.1, 6.1.4, 6.3.4, 7.1.3, 8.4.2
9.2 Task Types and Task Objects 6.1.1, 6.1.2
9.3 Task Execution - Task Activation 6.1.3, 6.1.4, 6.1.5, 6.3.2,
7.4.1, 7.4.2, 7.4.5, 8.2.6
9.4 Task Dependence - Termination of Tasks 6.2.3, 6.3
9.5 Entries, Entry Calls, and Accept Statements 3.2.4, 4.3.1,
5.1.4, 5.2.1, 5.2.4, 5.6.1, 5.9.4,
6.1.4, 6.2, 6.3.1, 6.3.2, 7.4.5, 7.4.7
9.6 Delay Statements, Duration, and Time 6.1.5, 6.2.5, 7.1.1,
7.4.2, 7.4.3
9.7 Select Statements 2.1.2, 5.6.1, 6.2.1, 6.2.6
9.7.1 Selective Waits 6.2.1, 6.2.2, 6.2.4, 6.2.5, 6.3.2, 7.4.4
9.7.2 Conditional Entry Calls 4.2.4, 6.2.5
9.7.3 Timed Entry Calls 4.2.4, 6.1.5, 6.2.5
9.8 Priorities 6.1.4, 7.4.5, 8.4.2
9.9 Task and Entry Attributes 6.2.3
9.10 Abort Statements 6.3.3, 7.4.6
9.11 Shared Variables 6.2.4, 7.4.7
9.12 Example of Tasking
10 Program Structure and Compilation Issues 4.1.1
10.1 Compilation Units - Library Units 3.3.2, 4.1.1, 7.1.4
10.1.1 Context Clauses - With Clauses 2.1.2, 4.2.1, 4.2.3,
8.2.6, 8.4.1, 8.4.2
10.1.2 Examples of Compilation Units
10.2 Subunits of Compilation Units 4.1.1, 4.2.3
10.2.1 Examples of Subunits
10.3 Order of Compilation
10.4 The Program Library 8.4
10.5 Elaboration of Library Units 8.4.2
10.6 Program Optimization 8.4.4
11. Exceptions 4.3, 5.8, 7.5
11.1 Exception Declarations 3.3.5, 4.3.1, 5.4.3, 7.5.2, 7.5.3
11.2 Exception Handlers 2.1.2, 4.3.1, 5.6.9, 5.8.1, 5.8.2, 5.8.3,
5.8.4, 6.2.2, 6.3.1, 6.3.4, 7.5.2, 7.5.3,
8.2.7
11.3 Raise Statements 4.3.1, 7.5.3, 8.2.3, 8.2.7
11.4 Exception Handling 4.3.1, 5.8.1, 5.8.2, 5.8.3, 5.8.4
6.2.2, 6.3.4, 7.5, 8.2.7
11.4.1 Exceptions Raised During the Execution of Statements 5.8.1
11.4.2 Exceptions Raised During the Elaboration of Declarations 5.8.1
11.5 Exceptions Raised During Task Communication 5.8.1, 6.2.2
11.6 Exceptions and Optimization
11.7 Suppressing Checks 5.9.5
12. Generic Units
12.1 Generic Declarations 2.1.2, 3.2.4, 4.2.2
8.1.1, 8.1.2, 8.1.3, 8.3.1, 8.3.2,
8.3.3, 8.3.4, 8.3.5, 8.4.1, 8.4.3
12.1.1 Generic Formal Objects 8.2.4, 8.2.6
12.1.2 Generic Formal Types 8.2.2, 8.3.3, 8.3.4, 8.3.6
12.1.3 Generic Formal Subprograms 8.3.6
12.2 Generic Bodies 8.2.7
12.3 Generic Instantiation 2.1.2, 3.2.4, 5.2.2,
8.1.1, 8.2.4, 8.2.5, 8.3.2, 8.3.6, 8.4.4
12.3.1 Matching Rules for Formal Objects
12.3.2 Matching Rules for Formal Private Types 5.3.3, 8.3.4
12.3.3 Matching Rules for Formal Scalar Types
12.3.4 Matching Rules for Formal Array Types 8.2.2
12.3.5 Matching Rules for Formal Access Types
12.3.6 Matching Rules for Formal Subprograms
12.4 Example of a Generic Package
13. Representation Clauses and
Implementation-Dependent Features 4.1.4, 7.6
13.1 Representation Clauses 7.6.1
13.2 Length Clauses 5.4.3, 7.6.1
13.3 Enumeration Representation Clauses 3.4.2, 7.6.1
13.4 Record Representation Clauses 2.1.2, 7.6.1
13.5 Address Clauses 5.9.4
13.5.1 Interrupts 4.1.7, 5.9.4
13.6 Change of Representation 7.6.1
13.7 The Package System 7.6.2
13.7.1 System-Dependent Named Numbers 7.4.3
13.7.2 Representation Attributes 7.3.1
13.7.3 Representation Attributes of Real Types 7.2.3
13.8 Machine Code Insertions 7.1.5, 7.6.3
13.9 Interface to Other Languages 5.9.3, 7.1.5, 7.6.4, 7.6.7
13.10 Unchecked Programming
13.10.1 Unchecked Storage Deallocation 5.4.3, 5.9.2, 7.6.6
13.10.2 Unchecked Type Conversions 5.9.1, 7.6.7
14. Input-Output 7.7
14.1 External Files and File Objects
14.2 Sequential and Direct Files
14.2.1 File Management 7.7.1, 7.7.2
14.2.2 Sequential Input-Output 5.9.7, 7.7.3
14.2.3 Specification of the Package Sequential_IO
14.2.4 Direct Input-Output 5.9.7, 7.7.1, 7.7.3
14.2.5 Specification of the Package Direct_IO
14.3 Text Input-Output 4.2.2
14.3.1 File Management 7.7.2
14.3.2 Default Input and Output Files
14.3.3 Specification of Line and Page Lengths
14.3.4 Operations on Columns, Lines, and Pages
14.3.5 Get and Put Procedures
14.3.6 Input-Output of Characters and Strings
14.3.7 Input-Output for Integer Types
14.3.8 Input-Output for Real Types
14.3.9 Input-Output for Enumeration Types
14.3.10 Specification of the Package Text_IO 3.2.2, 4.2.2
14.4 Exceptions in Input-Output
14.5 Specification of the Package IO_Exceptions
14.6 Low Level Input-Output 7.7.4
14.7 Example of Input-Output
Annexes
A. Predefined Language Attributes 3.2.5, 3.4.2, 5.3.3, 5.5.1,
5.5.2, 6.2.3, 8.2.4
B. Predefined Language Pragmas 4.1.4, 5.9.5, 6.1.4, 6.2.4,
7.4.5, 7.4.7, 7.6.4, 9.1.1
C. Predefined Language Environment 3.4.1, 5.7.1, 6.1.5,
7.1.1, 7.1.6, 7.2.1, 7.5.1, 7.5.2, 7.6.3
Appendices
D. Glossary
E. Syntax Summary
F. Implementation-Dependent Characteristics 4.1.1, 5.4.1,
7.1.1, 7.1.2, 7.1.3, 7.1.5, 7.1.6,
7.2.1, 7.2.4, 7.4.3, 7.6.2, 7.6.3, 7.6.5, 7.6.8
REFERENCES
AIRMICS
1990
Software Reuse Guidelines, ASQB-GI-90-015. U.S. Army Institute for Research
in Management Information, Communications, and Computer Sciences.
Anderson, T. and R.W. Witty
1978
Safe Programming. BIT (Tidscrift Nordisk for Informations behandling) 18:1-8.
ARTEWG
1986
Catalogue of Ada Runtime Implementation Dependencies, draft version.
Association for Computing Machinery, Special Interest Group for Ada, Ada
Run-Time Environments Working Group.
Baker, Henry G.
1991
"A Heavy Thought..." Ada Letters. 11,2:45.
Barnes, J.G.P.
1989
Programming in Ada. third edition. Reading, MA.: Addison-Wesley.
Booch, G.
1986
Software Engineering with Ada. second edition. Menlo Park, CA: The
Benjamin/Cummings Publishing Company, Inc.
1987
Software Components with Ada - Structures, Tools and Subsystems. Menlo Park,
California: The Benjamin/Cummings Publishing Company, Inc.
CECOM
1989
CECOM "Final Report -- Catalogue of Ada Runtime Implementation Dependencies,"
CIN; C02092JNB0001.
Charette, R.N.
1986
Software Engineering Environments Concepts and Technology. Intertext
Publications Inc. New York: McGraw-Hill Inc.
Cohen, N.H.
1986
Ada as a Second Language. New York: McGraw-Hill Inc.
Conti, R.A.
1987
Critical Run-Time Design Tradeoffs in an Ada Implementation. Proceedings of
the Joint Ada Conference, Fifth National Conference on Ada Technology and
Washington Ada Symposium. pp. 486-495.
Department of Defense
1983
Reference Manual for the Ada Programming Language. ANSI/MIL-STD-1815A.
Edwards, S.
1990
An Approach for Constructing Reusable Software Components in Ada, IDA Paper
P-2378. Institute for Defense Analyses.
Foreman, J. and
J. Goodenough
1987
Ada Adoption Handbook: A Program Manager's Guide. Version 1.0, CMU/SEI-87-TR-9
ESD-TR-87-110. Software Engineering Institute.
Gonzalez, Dean W.
1991
"`=' Considered Harmful," Ada Letters. 11,2:56.
Goodenough, J., and
L. Sha
1988
The Priority Ceiling Protocol: A Method for Minimizing the Blocking of High
Priority Ada Tasks, Tech. Rep. SEI-SSR-4. Software Engineering Institute.
Griest
1989
"Limitations on the Portability of Real Time Ada Programs," Proceedings of the
1989 Tri-Ada conference, Tom Griest.
Honeywell
1986
A Guidebook for Writing Reusable Source Code in Ada. Corporate Systems
Development Division. Version 1.1. CSC-86-3:8213.
IEEE Dictionary
1984
IEEE Standard Dictionary of Electrical and Electronics Terms. ANSI/IEEE Std
100-1984.
MacLaren, L.
1980
Evolving Toward Ada in Real Time Systems. ACM Sigplan Notices. 15(11):146-155.
Matthews, E.R.
1987
Observations on the Portability of Ada I/O. ACM Ada Letters. VII(5):100-103.
Melliar-Smith, P.M. and B. Randell
1987
Software Reliability: The Role of Programmed Exception Handling. ACM Sigplan
Notices. 12(3):95-100.
NASA
1987
Ada Style Guide. Version 1.1, SEL-87-002. Goddard Space Flight Center:
Greenbelt, MD 20771.
NASA
1992
Ada Efficiency Guide. Technical Note 552-FDD-91/068R0UD0. NASA, Goddard
Space Flight Center: Greenbelt, MD 20771.
Nissen, J. and P. Wallis 1984
Portability and Style in Ada. Cambridge University Press.
Pappas, F.
1985
Ada Portability Guidelines. DTIC/NTIS #AD-A160 390.
Pyle, I.C.
1985
The Ada Programming Language. second edition. UK.: Prentice-Hall International.
Rosen, J. P.
1987
In Defense of the `Use' Clause. ACM Ada Letters. VII(7):77-81.
Ross, D.
1989
The Form of a Passive Iterator. ACM Ada Letters. IX(2):102-105.
Schneiderman, B.
1986
Empirical Studies of Programmers: The Territory, Paths and Destinations.
Empirical Studies of Programmers. ed. E. Soloway and S. Iyengar. pp. 1-12.
Norwood, NJ: Ablex Publishing Corp.
Soloway, E., J. Pinto,
S. Fertig, S. Letovsky,
R. Lampert, D. Littman,
and K. Ewing.
1986.
Studying Software Documentation From A Cognitive Perspective: A Status Report.
Proceedings of the Eleventh Annual Software Engineering Workshop. Report
SEL-86-006, Software Engineering Laboratory. Greenbelt, MD:NASA Goddard Space
Flight Center.
United Technologies
1987
CENC Programmer's Guide. Appendix A Ada Programming Standards.
Volz, R.A., Mudge, Naylor and Mayer.
1985
Some Problems in Distributing Real-time Ada Programs Across Machines. Ada in
Use, Proceedings of the Ada International Conference. pp. 14-16. Paris.
Wheeler, David A.
1992
Analysis and Guidelines for Reusable Ada Software. IDA Paper P-2765.
Alexandria, Virginia:Institute for Defense Analyses.
BIBLIOGRAPHY
ACVC (Ada Compiler Validation Capability). Ada Validation Facility, ASD/SIOL.
Wright-Patterson Air Force Base, OH.
AIRMICS. 1990. Software Reuse Guidelines, ASQB-GI-90-015. U.S. Army Institute
for Research in Management Information, Communications, and Computer Sciences.
Anderson, T. and R. W. Witty. 1978. Safe Programming. BIT (Tidscrift Nordisk
for Informations behandling) 18:1-8.
ARTEWG. November 5, 1986. Catalogue of Ada Runtime Implementation
Dependencies, draft version. Association for Computing Machinery, Special
Interest Group for Ada, Ada Run-Time Environments Working Group.
Bardin, Thompson. Jan-Feb 1988. Composable Ada Software Components and the
Re-Export Paradigm. ACM Ada Letters. VIII(1):58-79.
Bardin, Thompson. March-April 1988. Using the Re-Export Paradigm to Build
Composable Ada Software Components. ACM Ada Letters. VIII(2):39-54.
Barnes, J. G. P. 1989. Programming in Ada. third edition. Reading, MA.:
Addison-Wesley.
Booch, G. 1987. Software Components with Ada - Structures, Tools and
Subsystems. Menlo Park, CA.: The Benjamin/Cummings Publishing Company, Inc.
Booch, G. 1986. Software Engineering with Ada. second edition. Menlo Park,
CA.: The Benjamin/Cummings Publishing Company, Inc.
Brooks, F. B. 1975. The Mythical Man-Month. Essays on Software Engineering.
Reading, MA:Addison-Wesley.
CECOM. 1989. CECOM "Final Report -- Catalogue of Ada Runtime Implementation
Dependencies," CIN; C02092JNB0001.
Charette, R. N. 1986. Software Engineering Environments Concepts and
Technology. Intertext Publications Inc. New York: McGraw-Hill Inc.
Cohen, N. H. 1986. Ada as a Second Language. New York: McGraw-Hill Inc.
Conti, R. A. March 1987. Critical Run-Time Design Tradeoffs in an Ada
Implementation. Proceedings of the Joint Ada Conference, Fifth National
Conference on Ada Technology and Washington Ada Symposium. pp. 486-495.
Cristian, F. March 1984. Correct and Robust Programs. IEEE Transactions on
Software Engineering. SE-10(2):163-174.
Department of Defense, Ada Joint Program Office. 1984. Rationale for the
Design of the Ada Programming Language.
Department of Defense, Ada Joint Program Office. January 1983. Reference
Manual for the Ada Programming Language. ANSI/MIL-STD-1815A.
Edwards, S. 1990. An Approach for Constructing Reusable Software Components
in Ada, IDA Paper P-2378. Institute for Defense Analyses.
Foreman, J. and J. Goodenough. May 1987. Ada Adoption Handbook: A Program
Manager's Guide. Version 1.0, CMU/SEI-87-TR-9 ESD-TR-87-110. Software
Engineering Institute.
Gary, B. and D. Pokrass. 1985, Understanding Ada A Software Engineering
Approach. John Wiley & Sons.
Goodenough, J. B. March 1986. A Sample of Ada Programmer Errors. Unpublished
draft resident in the Ada Repository under file name
PD2:<ADA.EDUCATION>PROGERRS.DOC.2.
Herr, C. S. August 1987. Compiler Validation and Reusable Software. St. Louis:
a Report from the CAMP Project, McDonnell Douglas Astronautics Company.
International Workshop on Real-Time Ada Issues. 1987. ACM Ada Letters. VII(6).
Mortonhampstead, Devon, U.K.
International Workshop on Real-Time Ada Issues II. 1988. ACM Ada Letters.
VIII(6). Mortonhampstead, Devon, U.K.
Kernighan, B. and P. J. Plauger, 1978. The Elements of Programming Style. New
York: McGraw-Hill, Inc.
Matthews, E. R. September, October 1987. Observations on the Portability of
Ada I/O. ACM Ada Letters. VII(5):100-103.
MacLaren, L. November 1980. Evolving Toward Ada in Real Time Systems. ACM
Sigplan Notices. 15(11):146-155.
Melliar-Smith, P. M. and B. Randell. March 1987. Software Reliability: The Role
of Programmed Exception Handling. ACM Sigplan Notices. 12(3):95-100 .
Mowday, B. L. and E. Normand. November 1986. Ada Programming Standards.
General Dynamics Data Systems Division Departmental Instruction 414.717.
NASA. May 1987. Ada Style Guide. Version 1.1, SEL-87-002. Goddard Space Flight
Center: Greenbelt, MD 20771.
NASA. 1992. Ada Efficiency Guide. Technical Note 552-FDD-91/068R0UD0. NASA,
Goddard Space Flight Center: Greenbelt, MD 20771.
Nissen, J. C. D., P. Wallis, B. A., Wichmann, et al. 1982. Ada-Europe
Guidelines for the Portability of Ada Programs. ACM Ada Letters. I(3):44-61.
Nissen, J. and P. Wallis. 1984. Portability and Style in Ada. Cambridge
University Press.
Pappas, F. March 1985. Ada Portability Guidelines. DTIC/NTIS #AD-A160 390.
Pyle, I. C. 1985. The Ada Programming Language. second edition.
UK:Prentice-Hall International.
Rosen, J. P. November, December 1987. In Defense of the `Use' Clause. ACM Ada
Letters. VII(7):77-81.
Ross, D. March-April 1989. The Form of a Passive Iterator. ACM Ada Letters.
IX(2):102-105.
Rymer, J. and T. McKeever. September 1986. The FSD Ada Style Guide. IBM
Federal Systems Division Ada Coordinating Group.
Schneiderman, B. 1986. Empirical Studies of Programmers: The Territory, Paths
and Destinations. Empirical Studies of Programmers. ed. E. Soloway and S.
Iyengar. pp. 1-12. Norwood, NJ: Ablex Publishing Corp.
SofTech Inc. December 1985. ISEC Reusability Guidelines. Report 3285-4-247/2.
Also US Army Information Systems Engineering Command. Waltham MA.
Soloway, E., J. Pinto, S. Fertig, S. Letovsky, R. Lampert, D. Littman, K.
Ewing. December 1986. Studying Software Documentation From A Cognitive
Perspective: A Status Report. Proceedings of the Eleventh Annual Software
Engineering Workshop. Report SEL-86-006, Software Engineering Laboratory.
Greenbelt, MD:NASA Goddard Space Flight Center.
Stark M. and E. Seidewitz. March 1987. Towards A General Object-Oriented Ada
Lifecycle. In Proceedings of the Joint Ada Conference. Fifth National
Conference on Ada Technology and Washington Ada Symposium. 213-222.
St.Dennis, R. May 1986. A Guidebook for Writing Reusable Source Code in Ada
-Version 1.1. Report CSC-86-3:8213. Golden Valley, MN: Honeywell Corporate
Systems Development Division.
United Technologies. February 9, 1987. CENC Programmer's Guide. Appendix A Ada
Programming Standards.
VanNeste, K.F. January/February 1986. Ada Coding Standards and Conventions.
ACM Ada Letters. VI(1):41-48.
Volz, R. A., Mudge, Naylor and Mayer. May 1985. Some Problems in Distributing
Real-time Ada Programs Across Machines. Ada in Use, Proceedings of the Ada
International Conference. pp. 14-16. Paris.
Wheeler, David A. August 1992. Analysis and Guidelines for Reusable Ada
Software. IDA Paper P-2765. Alexandria, Virginia:Institute for Defense
Analyses.
INDEX
Symbols
'Base, 63
'Callable, 99
'Constrained, 63
'Count, 99
'First, 66, 67
'Image, 36
'Last, 66, 67
'Length, 67
'Pos, 36
'Pred, 36
'Range, 67
'Size, 63
'Storage_Size, 116
'Succ, 36
'Terminated, 99, 105
'Val, 36
'Value, 36
A
abbreviation, 19, 20
capitalization, 18
renames clause, 79
reusability, 129
abnormal termination, 106
abort
affects 'Count, 100
dynamic task, 94
portability, 118
statement, 106
abstract data, shared, 92
abstract data object, 139
abstract data type, 44, 139
abstract state machine, 139
abstraction, 47, 83
affect on naming, 128
complete functionality, 136
data modeling, 63
enhanced by subprogram, 42
includes exception, 51
name, 22
protect with exception handler, 82
supported, 41
using constrained subtype, 72
using nested record, 65
using private type, 63
using types, 61
accept
blocking, 96
causing deadlock, 105
closed, 97
end statement, 57
indentation, 8
minimize, 103
rendezvous, 96
access, 65
affects equality, 80
I/O portability, 123
portability, 121
access function, 45
accuracy
floating point, 114
fraction, 18
greater, 113
of constant, 115
of relational expression, 69
portability, 115
acronym, capitalization, 18
active iterator, 142
actual parameter, 58
anonymous, 62
adaptability, 136
address clause, 85
adjective, for boolean object, 21
aggregate, 77
function calls in, 43
algorithm
comment in header, 29
concurrent, 45
encapsulated in generic, 137
portability, 120
alias
of dynamic data, 65
of dynamic task, 94
using address clause, 85
alignment
and nesting, 70
declaration, 10
parameter modes, 11
vertical, 5, 9, 10, 11
allocate
dynamic data, 65
dynamic task, 93
ambiguous, use clause, 78
anonymous type, 62
task, 92
anticipated change
adaptability, 136
anonymous task, 93
comment in header, 29
encapsulate, 44
parameter mode, 60
apostrophe, spacing, 6
application domain
abbreviation, 19
abbreviation affects reusability, 129
application specific, literal, 24
application-independent, name, 128
array
anonymous type, 62
attribute, 67
function calls in initialization, 43
parallel, do not use, 64
parameter passing, 85, 131
performance, 154
range, 67
slice, 71
unconstrained, 131
assignment
for private type, 63
import for limited type, 145
reduced by aggregate, 77
assumption, 131
documented through generic parameter, 146
asynchronous
'Count, 99
entity, 91
attribute
'Base, 63
'Callable, 99
'Constrained, 63
'Count, 99
'First, 66, 67
'Image, 36
'Last, 66, 67
'Length, 67
'Pos, 36
'Pred, 36
'Range, 67
'Size, 63
'Storage_Size, 116
'Succ, 36
'Terminated, 99
'Val, 36
'Value, 36
implementation-defined, 121
in generic, 132
B
binary operator, spacing, 5
binding, 47
blank lines, 12
for goto statement, 75
for label, 75
return statement, 75
block
indentation, 7
localizing exception, 82
marker comment, 34, 35
name, 56
nesting, 56
performance, 154
statement, 76
to minimize suppress, 85
use clause, 78
blocking
not busy waiting, 96
with priority, 95
body
comment, 29
end statement, 57
header, as pagination, 13
hide interface to foreign code, 85
hide Unchecked_Conversion, 83
name repeated in begin, 34
task, 106
boolean
function, name, 22
object, name, 21
boundary value, 61
busy wait, 95
created with priorities, 103
portability, 117
C
Calendar, portability, 117
capitalization, 18
in numeric literal, 17
case statement, 71
indentation, 7
marker comment, 34
nesting, 70
use constrained subtype, 72
clause
address, 85
context
minimize, 46
visibility, 47
length, for dynamic data, 65
renames, 79
representation, 116, 119, 120
instead of enumeration, 36
use, 78, 79
closing file, portability, 123
code, formatting, 5
cohesion, 44
colon
alignment, 10, 12
spacing, 6
comma, spacing, 6
comment, 17, 24
body header, 29
data, 30
distinct from code, 33
do not repeat, 30
exception, 30
exception handler for other, 82
header, 25
file, 26
for group of routines, 28
purpose, 28
when to omit, 30
label, 75
machine code insert, 120
marker, 34
minimize, 24
numerical analysis, 115
program specification, 26
reduced by naming, 20, 24, 56, 57, 58
reduced by static expression, 24
return statement, 75
statement, 32
tasking implementation, 49
to document portability, 111
trailing, alignment, 10
type, 30
communication, 96
complexity, 103
using shared variable, 101
compilation
affected by Inline, 153
conditional, 149
separate, 41
complex communication, 103
complex data, comment, 30
concurrency, 91
See also task
affects exception behavior, 81
concurrent algorithm, 45, 91
conditional compilation, 149
conditional entry
call, 50, 102
indentation, 8
conditional expression, in while loop, 72
configuration control, 44
constant, 23
declaration, alignment, 10
in static expression, 131
to avoid use clause, 79
to reduce nesting, 70
type of, 24
constraint, 36, 60
constraint check
for generic formal, 133
performance, 155
Constraint_Error, portability, 119
context
dependency, 46
of exceptions, 50
to shorten names, 20
context clause
generic needs elaboration, 148
indentation, 8
minimize, 46
per line, 14
reduced using generic parameter, 147
visibility, 47
continuation condition, for while loop, 72
continuation line, indentation, 6, 7, 9
conversion
loss of precision, 115
of a private type, 63
rational fraction, 18
type, 61
unchecked, 83, 122
copyright notice, 26
coupling
data, 45
due to pragma, 148
reduced using generic parameter, 147
cyclic activity, 45
cyclic executive, termination, 106
D
dangling reference, 65
data
abstract type, 139
comment, 30
coupling, 45
dynamic, 65
grouped in package, 44
in algorithm, 137
iterator for complex, 142
protected, 101
representation, 120
shared, 45, 100
structure, 63
Unchecked_Conversion, 83
data-driven program, 150
dead code, 149
deadlock, 49
eliminate, 95
deallocation, 65
declaration
alignment, 10
anonymous task, 93
array size, 131
blank lines, 12
constant, 23
constrained subtype, 72
digits, 113
exception, 50
floating point, 114
function call in, 85
grouping, 44
hiding, 79
minimize, 46
named number, 23
per line, 14
range, 113
record, for heterogeneous data, 63
renames, 20, 78, 79
type, 20, 35
variable, 45
within block, 76
default mode, 60
default parameter, 59
delay
drift, 96
in selective wait, 102
inaccurate, 95
portability, 117
statement, 95
to avoid deadlock, 105
delimiter, spacing, 5
dependency
affects reusability, 147
comment about, 27
context, 46
reduced using generic parameter, 148
derived type, 35, 60
design
for reusability, 127
impact of concurrency, 91
impact of typing, 36
impact on nesting, 70
uses data representation, 120
digits declaration, 113
Direct_IO, 86
import private type, 147
discrete, affects equality, 80
discriminant, of a private type, 63
documentation
exception in abstraction, 50
generic formal parameter, 130
hidden task, 134
infinite loop, 75
invalid state, 135
object value, 21
of assumption, 131
of implementation dependency, 112
of iterator behavior, 142
of machine code, 120
of numerical analysis, 115
of system call, 122
portability, 111
table-driven code, 151
using named number, 131
drift, delay, 96
Duration, 110
portability, 117
dynamic data, 65
adaptability, 136
deallocation, 84
dynamic storage, 116
dynamic task, 93
E
Elaborate, 86
when to use, 148
elaboration, 85
of value, 23
else clause
comment, 34
nesting, 70
else part
in selective wait, 102
open alternative, 98
to avoid deadlock, 105
elsif clause, nesting, 70
encapsulation, 44
implementation dependency, 112, 119
of algorithm in generic, 137
of synchronization code, 101
supported, 41
end statement
name, 57
pagination, 13
entry
address clause, 85
attribute, 99
call, 49
avoiding Tasking_Error, 99
named association, 58
conditional, 50, 102
default parameter, 59
exceptions raised in, 50
hiding, 49
indentation, 8
interrupt, 85
minimize, 104
minimize accepts, 103
name, 22
parameter list, 58
parameter mode, 60
queue
'Count, 99
not prioritized, 94
timed, 50, 102
affects 'Count, 100
enumeration
alignment, 10, 11
in case statement, 71
literal, 78
type, 36
equality
for private type, 63
import for limited type, 145
overload operator, 80
erroneous execution, 83
not portable, 109
error
as an exception, 50
unrecoverable, use exception, 51
evaluation order
parenthesis, 67
select statement, 117
exception, 50, 80
avoiding, 81
cause, 82
comment, 27, 30
Constraint_Error, portability, 119
declaration, 50
do not propagate, 50
do not raise, 50
handler, 50, 81, 82
for Storage_Error, 65
in block, 76
in task body, 97, 106
reusability, 134
to avoid termination, 104
implementation-defined, 82, 119
in initialization, 86
keep error separate, 51
name, 50
Numeric_Error, portability, 119
part of abstraction, 51
portability, 118
predefined, 82, 118
Program_Error, 97
erroneous execution, 83
propagation, 82, 83, 134
reusability, 134
Storage_Error, 65
suppress, 85
Tasking_Error, 97, 99
user-defined, 82
execution pattern, 96
portable, 117
execution speed, 153
exit statement, 57, 72, 73
conditional, 69
in loop, 72
export, overloading in generic, 134
expression, 66
aggregate, 77
alignment, 9, 11
evaluation, portability, 115
function calls in, 43
logical, 68
nesting, 69
numeric, 113
order dependency, 87
parenthesis, 67
relational, 68, 69
slice, 71
spacing, 6
static, 23, 130
universal_real, 115
use named association, 58
F
family of parts, 149
file
closing, 123
header, 26
naming conventions, 41
organization, 41
finalization, complete functionality, 136
fixed point
in relational expression, 69
precision, 110
flag
in complex loop, 73
in while loop, 73
naming, 68
floating point
accuracy, 114, 115
affects equality, 80
arithmetic, 114
in relational expression, 69
model, 114, 115
precision, 110, 114
relational expression, 115
flow of control, 81
for loop, 72
indentation, 7
is bounded, 75
foreign code, 85
Form, parameter in predefined I/O, 123
formal parameter, 58
anonymous, 62
generic, 130
name, 58
formatter, 6, 9, 10, 11, 12, 13, 14, 15, 17, 19
FORTRAN, 64
equivalence, 85
fraction, 18
free list, 66
function
access, 45
body, indentation, 8
call
in dclaration, 85
named association, 58
recursive, 74
spacing, 6
default parameter, 59
end statement, 57
generic, 137, 147
Inline, 153
interrogative, to avoid exception, 50
name, 22
overload, 79
parameter list, 58
procedure versus, 43
return, 75
side effect, 43
specification, indentation, 8
to reduce nesting, 70
functionality, complete, 136
G
garbage collection
of dynamic data, 65
Unchecked_Deallocation, 84
generic, 137
abstract data object, 139
abstract data type, 139
instance, indentation, 8
name, 22, 128
named association, 58
package, 47, 134
parameter, 130
accessed within task, 134
indentation, 8
to reduce coupling, 147
robustness, 132
subtype in formal, 132
to encapsulate algorithm, 137
when formal raise exception, 134
global data, access, 46
global effect, comment, 27
goto, 75
simulated by exception, 81
guard
causing Program_Error, 97
evaluation order, 117
nonlocal variable, 100
referencing 'Count, 100
guideline, violation, 24
H
header, file, 26
heterogeneous data, 63
hidden task, 48, 134
avoid Priority, 148
hiding, declarations, 79
horizontal spacing, 5
I
identifier
abbreviation, 19, 129
capitalization, 18
constant, 23
naming convention, 20
numeric, 23
object, 21
reusability, 128
type, 20
use of underscore, 17
visibility, 46
if statement
avoid exit, 73
indentation, 7
marker comment, 34
nesting, 70
positive name, 68
immediately, undefined, 103
implementation
added feature, 113
comment in header, 29
encapsulate decisions, 44
hide detail, 46
implementation dependent, 119
actual limit, 111
at run time, 122
encapsulation, 112
global assumptions, 110
storage specification, 116
tentative rendezvous, 103
implementation-defined
attribute, 121
exception, 82, 119
pragma, 121
System constant, 120
Unchecked_Conversion, 84
in, 60
alignment, 12
in out, 60
for limited type, 145
used in generic formal, 132
incremental scheme, to improve performance, 155, 156
indentation, 6
affects abbreviation, 20
of declarations, 10
inequality, for private type, 63
infinite loop, 74
infix operator, 78, 79
information hiding, 41, 44
aids adaptability, 136
effects exception handling, 51
enforced through visibility, 46
using iterator, 144
using private type, 63
initialization, 85
in declaration, alignment, 10
performance, 154
procedure, 136
Inline, improves speed, 153
input/output
Low_Level_IO, 123
on access type, 123
portability, 122
instantiation
name, 22, 128
named association, 58
reusability, 137
Integer, portability, 110
Interface
portability, 120
to foreign language, 121
interface
access to device, 91
comment, 27
implementation-defined exception, 119
minimize, 46
parameter list, 58
to a device, 44, 63, 85, 123
to device data, 120
to foreign code, 85, 120, 121
undocumented, 47
interrupt
entry, 85
implementation dependent, 112
scheduling portability, 118
interval, delay, 96
iteration
bound, 74
using loop statement, 72
iterator, 142
L
label, 75
delimiter, 6
indentation, 7
late initialization, 154
length, line, 15
length clause, for dynamic data, 65
library
binding, 47
reuse, 79
separate compilation, 41
limited private type, 62
equality, 80
I/O difficult, 147
versus private, 145
line
continuation, indentation, 6
length, 15
multiple, 14
statements per, 14
linear independence, 24
literal
avoid in generic, 132
enumeration, 78
linear independence of, 23
numeric, 17, 66, 67
self-documenting, 24
string, 5
use named association, 58
localize
declaration, 76
implementation dependency, 112
scope, 78
logical operator, 68
loop, 72
array slices, 71
bound, 74
busy wait, 95, 103
conditional exit, 69
exit, 73
using relational expression, 69
indentation, 7
infinite, 74
marker comment, 35
name, 55, 57
nesting, 55, 57
Low_Level_IO, portability, 123
lower case, 18
M
machine code, not portable, 120
machine dependency, encapsulated, 44
main program, 112
marker comment, 34
membership test, of a private type, 63
memory management
of dynamic data, 65
of task, 94
Unchecked_Deallocation, 84
mod, performance, 155
mode
alignment, 11
explicit, 60
model
floating point, 115
task, 91
model interval
affects equality, 80
affects relational expression, 69, 116
modularity, 44
multiple return statement, 76
multiprocessor, 45, 92
mutual exclusion, and priority, 95
N
Name, parameter in predefined I/O, 123
name, 20
abbreviation, 19, 129
block, 56
boolean, 68
capitalization, 18
convention, 20
end statement, 57
flag, 68
formal parameter, 58
fully qualified, 20, 79
loop, 55, 57
nested record, 64
number, 23
object, 21
overloading, in generic, 134
predefined, 35
program, 22
qualified, 47
repeated in begin, 34
repeated in header, 26, 29
reusability, 128
simple, 17
state, 68
subtype, 35
type, 20, 35
use positive form, 68
named association, 58, 59
in aggregate, 77
named number, 23, 115, 130
to specify priority, 149
negative logic, in name, 68
nesting, 69
affects abbreviation, 20
block, 56
control structure, 70
expression, 69
indentation, 6
initialization exception, 86
loop, 55, 57
package, 47
record, 64
new, as an allocator, 65
non-terminating, tasks, 105
normal termination, 105
noun
as function name, 22
as identifier, 21
for record component, 21
to describe abstraction, 22
numeric
conversion, 18
encoding, 36
expression, 113
in relational expression, 69
literal, 17, 66, 67
named, 23
type, 113
Numeric_Error, portability, 119
O
object
identifier, 21
initialization, 85
name, 21
operating system, dependence, 122
operator
alignment, 9, 10
equality, 80
infix, 78
logical, 68
mod, 155
overload, 80
precedence, 6, 67, 70
rem, 155
renamed, 79
short circuit, 68
spacing, 5
optimizing compiler, values not checked, 86
optional parameter, 58
order
of arguments in aggregate, 77
of elaboration, 149
of evaluation, 43, 68, 87
in expression, 67
others clause
in case statement, 71
in exception, 50, 81
in task body, 106
out, 60
alignment, 12
not for limited type, 145
overload
equality, 80
in generic, 134
operator, 80
subprogram, 79
type name, 35
use clause, 78
P
package, 44
abstract data type, 139
body
comment, 29
file name, 41
for different environment, 112
hide Unchecked_Conversion, 83
hide Unchecked_Deallocation, 84
indentation, 8
multiple implementation, 42
using pragma Interface, 121
Calendar, portability, 117
cohesion, 44
comment, 26
coupling, 45
dependency, 47
Direct_IO, 86
document non-portable, 111
end statement, 57
generic, 47, 137, 147
implementation-defined exception, 119
interface, 112
Low_Level_IO, 123
minimize interface, 46
name, 22
named in use clause, 78
nested, 47
predefined
type name, 20
vendor supplied feature, 113
private, 62
Sequential_IO, 86
specification, 46, 47
exception declaration, 50
file name, 41
indentation, 8
pagination, 13
portability, 112
Standard, 35
predefined numeric type, 113
System
portability, 120
portability of Tick, 117
Text_IO, 47
vendor supplied, 113
pagination, 13, 34
paragraphing, 7
parameter
adding to list, 59
aggregate, 77
alignment, 11
anonymous, 62
array, 131
declaration, per line, 14
default value, 59
for main program, 112
formal name, 58
generic formal, 130
generic reduces coupling, 147
in predefined I/O, 123
list, 58
mode, 60
alignment, 11
name in header, 27
named association, 58
number, 46
optional, 58
passing mechanism, 84
with exception, 135
profile, 79
size, 131
unmodified with exception, 134
parameter type, alignment, 12
parenthesis, 67
alignment, 11
spacing, 6
superfluous, 6
parser, use table-driven program, 150
part family, 149
passive iterator, 142
performance, 153
access to data, 45
comment in header, 27
named number, 24
period, spacing, 6
periodic activity, 96
persistent object, 47
plain loop, 72
pointer
See also access
to task, 94
polling, 95
portability, 117
portability, 15, 44, 109
comment in header, 29
execution pattern, 96
of relational expression, 69
order dependency, 87
principles, 109
positional association, 59, 77
positive logic, naming, 68
pragma
Elaborate, 86, 148
implementation-defined, 121
Inline, 153
Interface
portability, 120
to foreign code, 121
introduce coupling, 148
Priority, 94, 148
portability, 118
Shared, 101
portability, 118
Suppress, 85
precedence of operator, 67, 70
precision
fixed point, 110
floating point, 110, 114
predefined
exception
do not raise, 50
handle, 82
portability, 118
I/O parameter, 123
predefined type, 35
as a name, 20
numeric, 113
String index, 114
predicate
as function name, 22
for boolean object, 21
preemptive scheduling, 118
prioritized activity, 45
Priority, 94
can create busy wait, 103
not for hidden task, 148
portability, 118
priority inversion, 95
private type, 62
equality, 80
for numeric type, 113
versus limited, 145
problem domain, model with task, 91
procedure
as main program, 112
call
named association, 58
recursive, 74
default parameter, 59
end statement, 57
generic, 137, 147
Inline, 153
name, 22
overload, 79
parameter list, 58
parameter mode, 60
return, 75
to reduce nesting, 70
versus function, 43
processor
multiple, 92
virtual, 92
program
body, indentation, 8
grouping, 44
name, 22
pagination, 13
termination, 82, 106
visibility, 47
Program_Error, 97
erroneous execution, 83
project
abbreviation, 20
entry name, 23
prologues, as pagination, 13
propagation, 82
exception, 83, 134
Q
qualified name, 47, 78, 79
queue, entry not prioritized, 95
R
race condition, 49, 105
attribute, 99
in tentative rendezvous, 103
priority, 95
with shared variable, 101
radix, 17
precision, 115
raise statement, in abstraction, 50
range
constraint, 60
declaration, 113
Duration, 110
in case statement, 71
scalar types, 35
values, 66
real operand, in relational expression, 69
recompilation, reduced by separate file, 42
record
assignment, 77
component, name, 21
heterogeneous data, 63
indentation, 8
map to device data, 63
nesting, 64
parameter passing, 85
with access type component, 65
recursion, bound, 74
relational expression, 68, 69
portability, 115
rem, performance, 155
renames clause, 20, 78, 79
declare in block, 76
for a type, 36
rendezvous
efficient, 96
exception during, 97
instead of shared variable, 100
tentative, 102
versus shared variable, 118
with exception handler, 104
repeat until, how to simulate, 73
representation clause
evaluate during porting, 120
for device data, 64
indentation, 9
instead of enumeration, 36
portability, 116, 119, 120
reserved word, capitalization, 18
return statement, 75
reusability, 127
library, 79
of non-terminating program, 106
renames clause, 79
robust software, 130
run time system, dependence, 122
runaway task, 106
S
safe numbers, for floating point, 114
safe programming, 75
scheduling
algorithm
affected by hidden task, 134
portability, 118
delay, 96
task, 106
using priority, 95
scientific notation, 17
scope
access type, portability, 121
exception, 50
minimize, 47
use clause, 78
select statement
blocking, 96
minimize, 103
portability, 117
to provide normal termination, 105
selected component, of a private type, 63
selective wait
closed alternatives, 97
efficient, 96
indentation, 8
with else, 102
semicolon, spacing, 6
sentinel value, 61
separate
compilation, 41
indentation, 8
to reduce visibility, 48
separation of concerns, 46
Sequential_IO, 86
import private type, 147
Shared, 101
portability, 118
shared data, 45
hidden task in generic, 134
shared variable, 100
portability, 118
short circuit operator, 68
side effect, 43, 69
simplification heuristics, 70
slice, 71
source text, 5, 110
spacing, 5
for goto statement, 75
for label, 75
horizontal, 5
specification
comment, 26
end statement, 57
generic, 132
header, as pagination, 13
indentation, 8
package, 46, 47
reusable part family, 149
task hiding, 49, 134
spelling, 17
abbreviation, 19
in comments, 25
Standard, 35
predefined numeric type, 113
starvation, 49
state
and exception, 50
naming, 68
statement, 69
abort, 106, 118
accept, 97, 103
block, 76
case, 71
comment, 32
delay, 95, 117
end, 57
exit, 57, 72, 73
goto, 75
indentation, 6
loop, 71
conditional exit, 69
marker comment, 34
per line, 14
return, 75
select, 97, 103
portability, 117
tentative rendezvous, 102
static
data, 65
expression, 23, 130
Storage_Error, 65
String, indexed with subtype, 114
strong typing, 35, 41, 60
to enforce assumption, 131
to reduce constraint check, 155
subprogram, 42
call
named association, 58
overhead, 43
recursive, 74
default parameter, 59
document non-portable, 111
end statement, 57
exceptions raised in, 50
generic, 137, 147
grouped in package, 44
grouping, 41
hiding task entries, 49
Inline, 153
main, 112
name, 22
overload, 79
overloading in generic, 134
parameter list, 46, 58
procedure versus function, 43
return, 75
to reduce nesting, 70
subrecord, 64
subtype, 35, 60
in case statement, 72
in generic, 132
used as range, 66
subunit
embedded, pagination, 13
file name, 41
indentation, 8
minimize context, 47
visibility, 47
Suppress, 85
symbolic, value, 23
synchronization, 91
and abort statement, 106
portability, 118
using shared variable, 100
System
portability, 120
portability of Tick, 117
system, do not comment, 27
T
tab character, 7
table-driven program, 150
task, 45, 91
abort dynamic, 94
activation order, 117
allocate, 93
anonymous type, 92
attribute, 99
avoid termination, 104
body
exception handler, 106
indentation, 8
order, 94
communication, 49, 96, 97
complexity, 103
portability, 118
declaration, 92
document non-portable, 111
dynamic, 93
end statement, 57
hidden, 48, 134, 148
comment in header, 27
model, 91
name, 22
non-terminating, 105
parameter passing, 85
portability, 117
receiving interrupt, 112
rendezvous, 96
runaway, 106
scheduling, 106
portability, 118
specification
indentation, 8
pagination, 13
synchronization, 100
point, 118
portability, 118
termination, 104, 105
type, 92
Tasking_Error, 97, 99
temporary file, portability, 123
tentative rendezvous, 102
terminate alternative, to avoid deadlock, 105
termination, 104
abnormal, 106
controlled, 82
dynamic task, 94
file status, 123
normal, 105
of loop, 73
Text_IO, 47
thread of control, 92
Tick, portability, 117
time sliced, scheduling, 118
timed entry, 102
affects 'Count, 100
call, 50
indentation, 8
timing
affected by task, 94
execution pattern, 96
tool, 5, 25, 79
type, 60
access, 65
portability, 121
anonymous, 62
anonymous task, 92
attribute, for real value, 115
boundary value, 61
choice affects equality, 80
comment, 30
conversion, 61
precision, 115
declaration, 35
derived, 35, 60
Duration, 110
portability, 117
enumeration, 36
floating point, model, 114
grouped in package, 44
identifier, 20
Integer, 110
limited private, 62, 145
name, 20, 35
numeric, 113
of constant, 24
parameter passing, 85
predefined, 35, 113
private, 62, 145
renaming, 36
strong, 35, 41, 60
subtype, 60
suffix, 20
unconstrained array, 131
universal, 24
used as range, 66
U
Unchecked_Conversion, 83
portability, 122
Unchecked_Deallocation, 84
portability, 121
underscore, 17
in file name, 41
in numbers, 17
significant in name, 20
universal_integer, 24
portability, 110
universal_real, 24
for greater precision, 115
until loop, how to simulate, 73
upper case, 18
use clause, 78, 79
See also context clause
user-defined exception, 82
replaces implementation-defined, 119
V
variable
declaration, alignment, 10
localize declaration, 77
of access type, 65
referenced in guard, 100
replaced with aggregate, 77
to reduce nesting, 70
valid state, 134
verb
as entry name, 22
as procedure name, 22
vertical alignment, 9, 10, 11
See also alignment
virtual processor, 92
visibility, 46, 47, 77
using renames clause, 79
W
while loop
See also loop
indentation, 7
with clause. See context clause
Z
zero based indexing, 154